@poncho-ai/harness 0.61.0 → 0.61.1

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.61.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.61.1 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
  CLI tsup v8.5.1
9
9
  CLI Target: es2022
10
10
  ESM Build start
11
+ ESM dist/index.js 580.50 KB
11
12
  ESM dist/isolate-F2PPSUL6.js 53.82 KB
12
- ESM dist/index.js 579.17 KB
13
- ESM ⚡️ Build success in 267ms
13
+ ESM ⚡️ Build success in 227ms
14
14
  DTS Build start
15
- DTS ⚡️ Build success in 8217ms
16
- DTS dist/index.d.ts 108.62 KB
15
+ DTS ⚡️ Build success in 7778ms
16
+ DTS dist/index.d.ts 110.56 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.61.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`a16f4fa`](https://github.com/cesr/poncho-ai/commit/a16f4fa32f0809d6ff73f0041cb0e0a3fb980bb1) Thanks [@cesr](https://github.com/cesr)! - Fix: checkpoint-resume no longer drops a turn's user message from the model
8
+ transcript. Extract the checkpoint→transcript assembly the orchestrator's
9
+ `resumeRunFromCheckpoint` used into shared, exported helpers —
10
+ `assembleCheckpointMessages`, `buildToolResultMessage`, `buildResumeCheckpoints`
11
+ — so external embedders (which execute gated tools themselves) reconstruct the
12
+ canonical transcript identically instead of re-deriving the index arithmetic
13
+ and drifting. Add a transcript-integrity guard in `applyTurnMetadata` that
14
+ logs when a finalize would leave the latest user message out of the
15
+ model-facing transcript.
16
+
3
17
  ## 0.61.0
4
18
 
5
19
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -2222,6 +2222,40 @@ declare const buildApprovalCheckpoints: ({ approvals, runId, checkpointMessages,
2222
2222
  pendingToolCalls: PendingToolCall[];
2223
2223
  kind?: "approval" | "device";
2224
2224
  }) => NonNullable<Conversation["pendingApprovals"]>;
2225
+ /** Reconstruct the full canonical transcript a checkpoint resumes from: the
2226
+ * prior history (before the checkpointed turn) + the checkpoint delta.
2227
+ * Handles both storage conventions via `normalizeApprovalCheckpoint` — the
2228
+ * initial checkpoint (base = prior length, delta) and a resume-created one
2229
+ * (base 0, full canonical). */
2230
+ declare const assembleCheckpointMessages: (conversation: Conversation, checkpoint: StoredApproval) => Message[];
2231
+ /** Pair an assistant tool-call message's `tool_calls` with externally-computed
2232
+ * results into the single `role:"tool"` message a continuation reads.
2233
+ * Returns undefined when `assistantMsg` isn't an assistant message with
2234
+ * parseable tool calls. Used to persist the resume's canonical history so it
2235
+ * matches what `continueFromToolResult` fed the model. Missing results
2236
+ * default to the deferred-error marker (same text the run uses). */
2237
+ declare const buildToolResultMessage: (assistantMsg: Message | undefined, toolResults: Array<{
2238
+ callId: string;
2239
+ toolName: string;
2240
+ result?: unknown;
2241
+ error?: string;
2242
+ }>) => Message | undefined;
2243
+ /** Build the `pendingApprovals` rows for a checkpoint reached DURING a resume
2244
+ * continuation. Stores the WHOLE canonical history the continuation ran with
2245
+ * (`priorMessages` = prior + tool result + new delta) with
2246
+ * `baseMessageCount: 0`. Keeping the full history here — rather than a delta
2247
+ * + a base count into a different message array — is what lets the next
2248
+ * resume reconstruct with no index arithmetic. */
2249
+ declare const buildResumeCheckpoints: ({ priorMessages, checkpointEvent, runId, kind, }: {
2250
+ priorMessages: Message[];
2251
+ checkpointEvent: {
2252
+ approvals: ApprovalEventItem[];
2253
+ checkpointMessages: Message[];
2254
+ pendingToolCalls: PendingToolCall[];
2255
+ };
2256
+ runId: string;
2257
+ kind?: "approval" | "device";
2258
+ }) => NonNullable<Conversation["pendingApprovals"]>;
2225
2259
  declare const applyTurnMetadata: (conv: Conversation, meta: TurnResultMetadata, opts?: {
2226
2260
  clearContinuation?: boolean;
2227
2261
  clearApprovals?: boolean;
@@ -2503,4 +2537,4 @@ type NewEntryNoId = NewConversationEntry extends infer T ? T extends NewConversa
2503
2537
  */
2504
2538
  declare const appendEntriesSafe: (store: ConversationStore, conversation: Pick<Conversation, "conversationId" | "ownerId" | "tenantId">, entries: NewEntryNoId[], log: Logger) => Promise<ConversationEntry[]>;
2505
2539
 
2506
- export { type ActiveConversationRun, type ActiveSubagentRun, type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, AgentOrchestrator, type ApprovalEventItem, type ArchivedToolResult$1 as ArchivedToolResult, type BashConfig, BashEnvironmentManager, type BashExecutionLimits, type BuiltInToolToggles, CALLBACK_LOCK_STALE_MS, type CallbackStartedEntry, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type ContinuationHooks, type Conversation, type ConversationCreateInit, type ConversationEntry, type ConversationState, type ConversationStatusSnapshot, type ConversationStore, type ConversationSummary, type CreateSkillToolsOptions, type CronJobConfig, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_NAME, DEFAULT_MAX_STEPS, DEFAULT_MODEL_NAME, DEFAULT_MODEL_PROVIDER, DEFAULT_TAGLINE, DEFAULT_TEMPERATURE, DEFAULT_TIMEOUT, type DefaultAgentDefinitionOptions, type EventSink, type ExecuteTurnResult, type HarnessOptions, type HarnessRunOutput, type HistorySource, InMemoryConversationStore, InMemoryEngine, InMemoryStateStore, type IsolateBinding, type IsolateConfig, LocalMcpBridge, LocalUploadStore, MAX_CONCURRENT_SUBAGENTS, MAX_CONTINUATION_COUNT, MAX_SUBAGENT_CALLBACK_COUNT, MAX_SUBAGENT_NESTING, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, type MountProvider, type NetworkConfig, type NewConversationEntry, OPENAI_CODEX_CLIENT_ID, type OpenAICodexAuthConfig, type OpenAICodexDeviceAuthRequest, type OpenAICodexSession, type OrchestratorHooks, type OrchestratorOptions, type OtlpConfig, type OtlpOption, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentApproval, type PendingSubagentResult, type PendingToolCall, type PonchoConfig, PonchoFsAdapter, PostgresEngine, type ProviderConfig, type Recurrence, type RecurrenceType, type Reminder, type ReminderCreateInput, type ReminderStatus, type ReminderStore, type RemoteMcpServerConfig, type RunConversationTurnOpts, type RunConversationTurnResult, type RunOutcome, type RunRequest, type RuntimeRenderContext, S3UploadStore, STALE_SUBAGENT_THRESHOLD_MS, STORAGE_SCHEMA_VERSION, type SecretsStore, type SkillContextEntry, type SkillMetadata, type SkillSource, SqliteEngine, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type StorageEngine, type StorageFactoryOptions, type StorageProvider, type StoredApproval, type SubagentManager, type SubagentResult, type SubagentResultEntry, type SubagentSpawnResult, type SubagentSummary, type SubagentTaskOutcome, type SubagentTranscript, type SubagentTranscriptMode, TOOL_RESULT_ARCHIVE_PARAM, type TelemetryConfig, TelemetryEmitter, type TenantTokenPayload, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type TurnDraftState, type TurnResultMetadata, type TurnSection, type UploadStore, type UploadsConfig, VFS_SCHEME, VercelBlobUploadStore, type VfsDirEntry, type VfsStat, type VirtualMount, abnormalEndResponse, appendEntriesSafe, applyTurnMetadata, buildAgentDirectoryName, buildApprovalCheckpoints, buildAssistantMetadata, buildSkillContextWindow, buildToolCompletedText, cloneSections, compactMessages, completeOpenAICodexDeviceAuth, computeNextOccurrence, createBashTool, createConversationStore, createConversationStoreFromEngine, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryStoreFromEngine, createMemoryTools, createModelProvider, createReminderStore, createReminderStoreFromEngine, createReminderTools, createSearchTools, createSecretsStore, createSkillTools, createStateStore, createStorageEngine, createSubagentTools, createTodoStoreFromEngine, createTurnDraftState, createUploadStore, createWriteTool, decodeFileInputData, defaultAgentDefinition, deleteOpenAICodexSession, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, executeConversationTurn, findSafeSplitPoint, findSafeSplitPointByTurns, flushTurnDraft, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getOpenAICodexAccessToken, getOpenAICodexAuthFilePath, getOpenAICodexRequiredScopes, getPendingSubagentResults, getPonchoStoreRoot, isMessageArray, jsonSchemaToZod, lastAssistantText, loadCanonicalHistory, loadPonchoConfig, loadRunHistory, loadSkillContext, loadSkillInstructions, loadSkillMetadata, loadSkillMetadataFromDirs, loadVfsSkillMetadata, mergeSkills, normalizeApprovalCheckpoint, normalizeOtlp, normalizeScriptPolicyPath, normalizeToolAccess, parseAgentFile, parseAgentMarkdown, parseSkillFrontmatter, ponchoDocsTool, readOpenAICodexSession, readSkillResource, realResponseText, recordStandardTurnEvent, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveEnv, resolveMemoryConfig, resolveRunRequest, resolveSkillDirs, resolveStateConfig, runConversationTurn, slugifyStorageComponent, startOpenAICodexDeviceAuth, stripPillMetaTokens, verifyTenantToken, withToolResultArchiveParam, writeOpenAICodexSession };
2540
+ export { type ActiveConversationRun, type ActiveSubagentRun, type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, AgentOrchestrator, type ApprovalEventItem, type ArchivedToolResult$1 as ArchivedToolResult, type BashConfig, BashEnvironmentManager, type BashExecutionLimits, type BuiltInToolToggles, CALLBACK_LOCK_STALE_MS, type CallbackStartedEntry, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type ContinuationHooks, type Conversation, type ConversationCreateInit, type ConversationEntry, type ConversationState, type ConversationStatusSnapshot, type ConversationStore, type ConversationSummary, type CreateSkillToolsOptions, type CronJobConfig, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_NAME, DEFAULT_MAX_STEPS, DEFAULT_MODEL_NAME, DEFAULT_MODEL_PROVIDER, DEFAULT_TAGLINE, DEFAULT_TEMPERATURE, DEFAULT_TIMEOUT, type DefaultAgentDefinitionOptions, type EventSink, type ExecuteTurnResult, type HarnessOptions, type HarnessRunOutput, type HistorySource, InMemoryConversationStore, InMemoryEngine, InMemoryStateStore, type IsolateBinding, type IsolateConfig, LocalMcpBridge, LocalUploadStore, MAX_CONCURRENT_SUBAGENTS, MAX_CONTINUATION_COUNT, MAX_SUBAGENT_CALLBACK_COUNT, MAX_SUBAGENT_NESTING, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, type MountProvider, type NetworkConfig, type NewConversationEntry, OPENAI_CODEX_CLIENT_ID, type OpenAICodexAuthConfig, type OpenAICodexDeviceAuthRequest, type OpenAICodexSession, type OrchestratorHooks, type OrchestratorOptions, type OtlpConfig, type OtlpOption, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentApproval, type PendingSubagentResult, type PendingToolCall, type PonchoConfig, PonchoFsAdapter, PostgresEngine, type ProviderConfig, type Recurrence, type RecurrenceType, type Reminder, type ReminderCreateInput, type ReminderStatus, type ReminderStore, type RemoteMcpServerConfig, type RunConversationTurnOpts, type RunConversationTurnResult, type RunOutcome, type RunRequest, type RuntimeRenderContext, S3UploadStore, STALE_SUBAGENT_THRESHOLD_MS, STORAGE_SCHEMA_VERSION, type SecretsStore, type SkillContextEntry, type SkillMetadata, type SkillSource, SqliteEngine, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type StorageEngine, type StorageFactoryOptions, type StorageProvider, type StoredApproval, type SubagentManager, type SubagentResult, type SubagentResultEntry, type SubagentSpawnResult, type SubagentSummary, type SubagentTaskOutcome, type SubagentTranscript, type SubagentTranscriptMode, TOOL_RESULT_ARCHIVE_PARAM, type TelemetryConfig, TelemetryEmitter, type TenantTokenPayload, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type TurnDraftState, type TurnResultMetadata, type TurnSection, type UploadStore, type UploadsConfig, VFS_SCHEME, VercelBlobUploadStore, type VfsDirEntry, type VfsStat, type VirtualMount, abnormalEndResponse, appendEntriesSafe, applyTurnMetadata, assembleCheckpointMessages, buildAgentDirectoryName, buildApprovalCheckpoints, buildAssistantMetadata, buildResumeCheckpoints, buildSkillContextWindow, buildToolCompletedText, buildToolResultMessage, cloneSections, compactMessages, completeOpenAICodexDeviceAuth, computeNextOccurrence, createBashTool, createConversationStore, createConversationStoreFromEngine, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryStoreFromEngine, createMemoryTools, createModelProvider, createReminderStore, createReminderStoreFromEngine, createReminderTools, createSearchTools, createSecretsStore, createSkillTools, createStateStore, createStorageEngine, createSubagentTools, createTodoStoreFromEngine, createTurnDraftState, createUploadStore, createWriteTool, decodeFileInputData, defaultAgentDefinition, deleteOpenAICodexSession, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, executeConversationTurn, findSafeSplitPoint, findSafeSplitPointByTurns, flushTurnDraft, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getOpenAICodexAccessToken, getOpenAICodexAuthFilePath, getOpenAICodexRequiredScopes, getPendingSubagentResults, getPonchoStoreRoot, isMessageArray, jsonSchemaToZod, lastAssistantText, loadCanonicalHistory, loadPonchoConfig, loadRunHistory, loadSkillContext, loadSkillInstructions, loadSkillMetadata, loadSkillMetadataFromDirs, loadVfsSkillMetadata, mergeSkills, normalizeApprovalCheckpoint, normalizeOtlp, normalizeScriptPolicyPath, normalizeToolAccess, parseAgentFile, parseAgentMarkdown, parseSkillFrontmatter, ponchoDocsTool, readOpenAICodexSession, readSkillResource, realResponseText, recordStandardTurnEvent, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveEnv, resolveMemoryConfig, resolveRunRequest, resolveSkillDirs, resolveStateConfig, runConversationTurn, slugifyStorageComponent, startOpenAICodexDeviceAuth, stripPillMetaTokens, verifyTenantToken, withToolResultArchiveParam, writeOpenAICodexSession };
package/dist/index.js CHANGED
@@ -12772,6 +12772,7 @@ var resolveRunRequest = (conversation, request) => {
12772
12772
  };
12773
12773
 
12774
12774
  // src/orchestrator/turn.ts
12775
+ import { randomUUID as randomUUID6 } from "crypto";
12775
12776
  var createTurnDraftState = () => ({
12776
12777
  assistantResponse: "",
12777
12778
  toolTimeline: [],
@@ -12993,6 +12994,52 @@ var buildApprovalCheckpoints = ({
12993
12994
  pendingToolCalls,
12994
12995
  kind
12995
12996
  }));
12997
+ var assembleCheckpointMessages = (conversation, checkpoint) => {
12998
+ const n = normalizeApprovalCheckpoint(checkpoint, conversation.messages);
12999
+ const base = n.baseMessageCount != null ? conversation.messages.slice(0, n.baseMessageCount) : [];
13000
+ return [...base, ...n.checkpointMessages ?? []];
13001
+ };
13002
+ var buildToolResultMessage = (assistantMsg, toolResults) => {
13003
+ if (assistantMsg?.role !== "assistant") return void 0;
13004
+ let toolCalls = [];
13005
+ try {
13006
+ const parsed = JSON.parse(typeof assistantMsg.content === "string" ? assistantMsg.content : "");
13007
+ toolCalls = parsed.tool_calls ?? [];
13008
+ } catch {
13009
+ return void 0;
13010
+ }
13011
+ if (toolCalls.length === 0) return void 0;
13012
+ const provided = new Map(toolResults.map((r) => [r.callId, r]));
13013
+ return {
13014
+ role: "tool",
13015
+ content: JSON.stringify(
13016
+ toolCalls.map((tc) => {
13017
+ const r = provided.get(tc.id);
13018
+ return {
13019
+ type: "tool_result",
13020
+ tool_use_id: tc.id,
13021
+ tool_name: r?.toolName ?? tc.name,
13022
+ content: r ? r.error ? `Tool error: ${r.error}` : JSON.stringify(r.result ?? null) : "Tool error: Tool execution deferred (pending approval checkpoint)"
13023
+ };
13024
+ })
13025
+ ),
13026
+ metadata: { timestamp: Date.now(), id: randomUUID6() }
13027
+ };
13028
+ };
13029
+ var buildResumeCheckpoints = ({
13030
+ priorMessages,
13031
+ checkpointEvent,
13032
+ runId,
13033
+ kind = "approval"
13034
+ }) => buildApprovalCheckpoints({
13035
+ approvals: checkpointEvent.approvals,
13036
+ runId,
13037
+ checkpointMessages: [...priorMessages, ...checkpointEvent.checkpointMessages],
13038
+ baseMessageCount: 0,
13039
+ pendingToolCalls: checkpointEvent.pendingToolCalls,
13040
+ kind
13041
+ });
13042
+ var messageText = (m) => typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((p) => p.text ?? "").join("") : "";
12996
13043
  var applyTurnMetadata = (conv, meta, opts = {}) => {
12997
13044
  const {
12998
13045
  clearContinuation = true,
@@ -13011,6 +13058,20 @@ var applyTurnMetadata = (conv, meta, opts = {}) => {
13011
13058
  } else if (shouldRebuildCanonical) {
13012
13059
  conv._harnessMessages = conv.messages;
13013
13060
  }
13061
+ if (isMessageArray(conv._harnessMessages) && conv._harnessMessages.length > 0) {
13062
+ const canonical = conv._harnessMessages;
13063
+ const summarized = canonical.some((m) => m.metadata?.isCompactionSummary);
13064
+ const lastUser = [...conv.messages].reverse().find((m) => m.role === "user");
13065
+ const lastUserText = lastUser ? messageText(lastUser).trim() : "";
13066
+ if (!summarized && lastUserText) {
13067
+ const present = canonical.some((m) => m.role === "user" && messageText(m).trim() === lastUserText);
13068
+ if (!present) {
13069
+ console.error(
13070
+ `[transcript-guard] conversation ${conv.conversationId}: model-facing transcript is missing the latest user message \u2014 it diverged from the display transcript. This is a resume/finalize message-assembly bug; the model will not see that turn's input.`
13071
+ );
13072
+ }
13073
+ }
13074
+ }
13014
13075
  if (meta.toolResultArchive !== void 0) {
13015
13076
  conv._toolResultArchive = meta.toolResultArchive;
13016
13077
  }
@@ -13041,12 +13102,12 @@ var STALE_SUBAGENT_THRESHOLD_MS = 5 * 60 * 1e3;
13041
13102
  import { createLogger as createLogger8, getTextContent as getTextContent3 } from "@poncho-ai/sdk";
13042
13103
 
13043
13104
  // src/orchestrator/entries-dual-write.ts
13044
- import { randomUUID as randomUUID6 } from "crypto";
13105
+ import { randomUUID as randomUUID7 } from "crypto";
13045
13106
  var appendEntriesSafe = async (store, conversation, entries, log2) => {
13046
13107
  if (entries.length === 0) return [];
13047
13108
  try {
13048
13109
  const withIds = entries.map(
13049
- (e) => ({ id: randomUUID6(), ...e })
13110
+ (e) => ({ id: randomUUID7(), ...e })
13050
13111
  );
13051
13112
  return await store.appendEntries(
13052
13113
  conversation.conversationId,
@@ -13285,34 +13346,11 @@ var AgentOrchestrator = class {
13285
13346
  });
13286
13347
  let latestRunId = conversation.runtimeRunId ?? "";
13287
13348
  let checkpointedRun = false;
13288
- const normalizedCheckpoint = normalizeApprovalCheckpoint(checkpoint, conversation.messages);
13289
- const baseMessages = normalizedCheckpoint.baseMessageCount != null ? conversation.messages.slice(0, normalizedCheckpoint.baseMessageCount) : [];
13290
- const fullCheckpointMessages = [...baseMessages, ...normalizedCheckpoint.checkpointMessages];
13291
- let resumeToolResultMsg;
13292
- const lastCpMsg = fullCheckpointMessages[fullCheckpointMessages.length - 1];
13293
- if (lastCpMsg?.role === "assistant") {
13294
- try {
13295
- const parsed = JSON.parse(typeof lastCpMsg.content === "string" ? lastCpMsg.content : "");
13296
- const cpToolCalls = parsed.tool_calls ?? [];
13297
- if (cpToolCalls.length > 0) {
13298
- const providedMap = new Map(toolResults.map((r) => [r.callId, r]));
13299
- resumeToolResultMsg = {
13300
- role: "tool",
13301
- content: JSON.stringify(cpToolCalls.map((tc) => {
13302
- const provided = providedMap.get(tc.id);
13303
- return {
13304
- type: "tool_result",
13305
- tool_use_id: tc.id,
13306
- tool_name: provided?.toolName ?? tc.name,
13307
- content: provided ? provided.error ? `Tool error: ${provided.error}` : JSON.stringify(provided.result ?? null) : "Tool error: Tool execution deferred (pending approval checkpoint)"
13308
- };
13309
- })),
13310
- metadata: { timestamp: Date.now() }
13311
- };
13312
- }
13313
- } catch {
13314
- }
13315
- }
13349
+ const fullCheckpointMessages = assembleCheckpointMessages(conversation, checkpoint);
13350
+ const resumeToolResultMsg = buildToolResultMessage(
13351
+ fullCheckpointMessages[fullCheckpointMessages.length - 1],
13352
+ toolResults
13353
+ );
13316
13354
  const fullCheckpointWithResults = resumeToolResultMsg ? [...fullCheckpointMessages, resumeToolResultMsg] : fullCheckpointMessages;
13317
13355
  let draftRef;
13318
13356
  let execution;
@@ -13357,12 +13395,10 @@ var AgentOrchestrator = class {
13357
13395
  const cpEvent = event;
13358
13396
  const conv = await this.conversationStore.get(conversationId);
13359
13397
  if (conv) {
13360
- conv.pendingApprovals = buildApprovalCheckpoints({
13361
- approvals: cpEvent.approvals,
13362
- runId: latestRunId,
13363
- checkpointMessages: [...fullCheckpointWithResults, ...cpEvent.checkpointMessages],
13364
- baseMessageCount: 0,
13365
- pendingToolCalls: cpEvent.pendingToolCalls
13398
+ conv.pendingApprovals = buildResumeCheckpoints({
13399
+ priorMessages: fullCheckpointWithResults,
13400
+ checkpointEvent: cpEvent,
13401
+ runId: latestRunId
13366
13402
  });
13367
13403
  conv.updatedAt = Date.now();
13368
13404
  await this.conversationStore.update(conv);
@@ -14610,7 +14646,7 @@ ${resultBody}`,
14610
14646
  };
14611
14647
 
14612
14648
  // src/orchestrator/run-conversation-turn.ts
14613
- import { randomUUID as randomUUID7 } from "crypto";
14649
+ import { randomUUID as randomUUID8 } from "crypto";
14614
14650
  import { createLogger as createLogger9 } from "@poncho-ai/sdk";
14615
14651
  var log = createLogger9("orchestrator");
14616
14652
  var runConversationTurn = async (opts) => {
@@ -14650,9 +14686,9 @@ var runConversationTurn = async (opts) => {
14650
14686
  const userMessage = {
14651
14687
  role: "user",
14652
14688
  content: userContent,
14653
- metadata: { id: randomUUID7(), timestamp: turnTimestamp }
14689
+ metadata: { id: randomUUID8(), timestamp: turnTimestamp }
14654
14690
  };
14655
- const assistantId = randomUUID7();
14691
+ const assistantId = randomUUID8();
14656
14692
  const draft = createTurnDraftState();
14657
14693
  let latestRunId = conversation.runtimeRunId ?? "";
14658
14694
  let runCancelled = false;
@@ -15028,11 +15064,14 @@ export {
15028
15064
  abnormalEndResponse,
15029
15065
  appendEntriesSafe,
15030
15066
  applyTurnMetadata,
15067
+ assembleCheckpointMessages,
15031
15068
  buildAgentDirectoryName,
15032
15069
  buildApprovalCheckpoints,
15033
15070
  buildAssistantMetadata,
15071
+ buildResumeCheckpoints,
15034
15072
  buildSkillContextWindow,
15035
15073
  buildToolCompletedText,
15074
+ buildToolResultMessage,
15036
15075
  cloneSections,
15037
15076
  compactMessages,
15038
15077
  completeOpenAICodexDeviceAuth,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.61.0",
3
+ "version": "0.61.1",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,6 +19,9 @@ export {
19
19
  executeConversationTurn,
20
20
  normalizeApprovalCheckpoint,
21
21
  buildApprovalCheckpoints,
22
+ assembleCheckpointMessages,
23
+ buildToolResultMessage,
24
+ buildResumeCheckpoints,
22
25
  applyTurnMetadata,
23
26
  type StoredApproval,
24
27
  type PendingToolCall,
@@ -10,6 +10,9 @@ import {
10
10
  applyTurnMetadata,
11
11
  normalizeApprovalCheckpoint,
12
12
  buildApprovalCheckpoints,
13
+ assembleCheckpointMessages,
14
+ buildToolResultMessage,
15
+ buildResumeCheckpoints,
13
16
  createTurnDraftState,
14
17
  recordStandardTurnEvent,
15
18
  type TurnDraftState,
@@ -421,38 +424,11 @@ export class AgentOrchestrator {
421
424
  let latestRunId = conversation.runtimeRunId ?? "";
422
425
  let checkpointedRun = false;
423
426
 
424
- const normalizedCheckpoint = normalizeApprovalCheckpoint(checkpoint, conversation.messages);
425
- const baseMessages = normalizedCheckpoint.baseMessageCount != null
426
- ? conversation.messages.slice(0, normalizedCheckpoint.baseMessageCount)
427
- : [];
428
- const fullCheckpointMessages = [...baseMessages, ...normalizedCheckpoint.checkpointMessages!];
429
-
430
- let resumeToolResultMsg: Message | undefined;
431
- const lastCpMsg = fullCheckpointMessages[fullCheckpointMessages.length - 1];
432
- if (lastCpMsg?.role === "assistant") {
433
- try {
434
- const parsed = JSON.parse(typeof lastCpMsg.content === "string" ? lastCpMsg.content : "");
435
- const cpToolCalls: Array<{ id: string; name: string }> = parsed.tool_calls ?? [];
436
- if (cpToolCalls.length > 0) {
437
- const providedMap = new Map(toolResults.map(r => [r.callId, r]));
438
- resumeToolResultMsg = {
439
- role: "tool",
440
- content: JSON.stringify(cpToolCalls.map(tc => {
441
- const provided = providedMap.get(tc.id);
442
- return {
443
- type: "tool_result",
444
- tool_use_id: tc.id,
445
- tool_name: provided?.toolName ?? tc.name,
446
- content: provided
447
- ? (provided.error ? `Tool error: ${provided.error}` : JSON.stringify(provided.result ?? null))
448
- : "Tool error: Tool execution deferred (pending approval checkpoint)",
449
- };
450
- })),
451
- metadata: { timestamp: Date.now() },
452
- };
453
- }
454
- } catch { /* last message is not a parseable assistant-with-tools -- skip */ }
455
- }
427
+ const fullCheckpointMessages = assembleCheckpointMessages(conversation, checkpoint);
428
+ const resumeToolResultMsg = buildToolResultMessage(
429
+ fullCheckpointMessages[fullCheckpointMessages.length - 1],
430
+ toolResults,
431
+ );
456
432
  const fullCheckpointWithResults = resumeToolResultMsg
457
433
  ? [...fullCheckpointMessages, resumeToolResultMsg]
458
434
  : fullCheckpointMessages;
@@ -505,12 +481,10 @@ export class AgentOrchestrator {
505
481
  };
506
482
  const conv = await this.conversationStore.get(conversationId);
507
483
  if (conv) {
508
- conv.pendingApprovals = buildApprovalCheckpoints({
509
- approvals: cpEvent.approvals,
484
+ conv.pendingApprovals = buildResumeCheckpoints({
485
+ priorMessages: fullCheckpointWithResults,
486
+ checkpointEvent: cpEvent,
510
487
  runId: latestRunId,
511
- checkpointMessages: [...fullCheckpointWithResults, ...cpEvent.checkpointMessages],
512
- baseMessageCount: 0,
513
- pendingToolCalls: cpEvent.pendingToolCalls,
514
488
  });
515
489
  conv.updatedAt = Date.now();
516
490
  await this.conversationStore.update(conv);
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import type { AgentEvent, Message } from "@poncho-ai/sdk";
2
3
  import type { Conversation } from "../state.js";
3
4
  import type { AgentHarness } from "../harness.js";
@@ -341,6 +342,110 @@ export const buildApprovalCheckpoints = ({
341
342
  kind,
342
343
  }));
343
344
 
345
+ // ── Checkpoint transcript reconstruction (single source of truth) ──
346
+ //
347
+ // Resuming a checkpointed turn requires rebuilding the FULL canonical model
348
+ // transcript from what was stored at the pause. Getting this right — in the
349
+ // canonical message space, not the display one — is subtle, and re-deriving
350
+ // it by hand in each embedder is exactly how the model-facing transcript
351
+ // drifted from the display transcript and silently dropped a turn's user
352
+ // message after an approval. These helpers are that logic, exported so the
353
+ // orchestrator's own resume AND external embedders (e.g. PonchOS, which
354
+ // executes gated tools itself) share one implementation that can't drift.
355
+
356
+ /** Reconstruct the full canonical transcript a checkpoint resumes from: the
357
+ * prior history (before the checkpointed turn) + the checkpoint delta.
358
+ * Handles both storage conventions via `normalizeApprovalCheckpoint` — the
359
+ * initial checkpoint (base = prior length, delta) and a resume-created one
360
+ * (base 0, full canonical). */
361
+ export const assembleCheckpointMessages = (
362
+ conversation: Conversation,
363
+ checkpoint: StoredApproval,
364
+ ): Message[] => {
365
+ const n = normalizeApprovalCheckpoint(checkpoint, conversation.messages);
366
+ const base = n.baseMessageCount != null ? conversation.messages.slice(0, n.baseMessageCount) : [];
367
+ return [...base, ...(n.checkpointMessages ?? [])];
368
+ };
369
+
370
+ /** Pair an assistant tool-call message's `tool_calls` with externally-computed
371
+ * results into the single `role:"tool"` message a continuation reads.
372
+ * Returns undefined when `assistantMsg` isn't an assistant message with
373
+ * parseable tool calls. Used to persist the resume's canonical history so it
374
+ * matches what `continueFromToolResult` fed the model. Missing results
375
+ * default to the deferred-error marker (same text the run uses). */
376
+ export const buildToolResultMessage = (
377
+ assistantMsg: Message | undefined,
378
+ toolResults: Array<{ callId: string; toolName: string; result?: unknown; error?: string }>,
379
+ ): Message | undefined => {
380
+ if (assistantMsg?.role !== "assistant") return undefined;
381
+ let toolCalls: Array<{ id: string; name: string }> = [];
382
+ try {
383
+ const parsed = JSON.parse(typeof assistantMsg.content === "string" ? assistantMsg.content : "");
384
+ toolCalls = parsed.tool_calls ?? [];
385
+ } catch {
386
+ return undefined;
387
+ }
388
+ if (toolCalls.length === 0) return undefined;
389
+ const provided = new Map(toolResults.map((r) => [r.callId, r]));
390
+ return {
391
+ role: "tool",
392
+ content: JSON.stringify(
393
+ toolCalls.map((tc) => {
394
+ const r = provided.get(tc.id);
395
+ return {
396
+ type: "tool_result",
397
+ tool_use_id: tc.id,
398
+ tool_name: r?.toolName ?? tc.name,
399
+ content: r
400
+ ? (r.error ? `Tool error: ${r.error}` : JSON.stringify(r.result ?? null))
401
+ : "Tool error: Tool execution deferred (pending approval checkpoint)",
402
+ };
403
+ }),
404
+ ),
405
+ metadata: { timestamp: Date.now(), id: randomUUID() },
406
+ };
407
+ };
408
+
409
+ /** Build the `pendingApprovals` rows for a checkpoint reached DURING a resume
410
+ * continuation. Stores the WHOLE canonical history the continuation ran with
411
+ * (`priorMessages` = prior + tool result + new delta) with
412
+ * `baseMessageCount: 0`. Keeping the full history here — rather than a delta
413
+ * + a base count into a different message array — is what lets the next
414
+ * resume reconstruct with no index arithmetic. */
415
+ export const buildResumeCheckpoints = ({
416
+ priorMessages,
417
+ checkpointEvent,
418
+ runId,
419
+ kind = "approval",
420
+ }: {
421
+ priorMessages: Message[];
422
+ checkpointEvent: {
423
+ approvals: ApprovalEventItem[];
424
+ checkpointMessages: Message[];
425
+ pendingToolCalls: PendingToolCall[];
426
+ };
427
+ runId: string;
428
+ kind?: "approval" | "device";
429
+ }): NonNullable<Conversation["pendingApprovals"]> =>
430
+ buildApprovalCheckpoints({
431
+ approvals: checkpointEvent.approvals,
432
+ runId,
433
+ checkpointMessages: [...priorMessages, ...checkpointEvent.checkpointMessages],
434
+ baseMessageCount: 0,
435
+ pendingToolCalls: checkpointEvent.pendingToolCalls,
436
+ kind,
437
+ });
438
+
439
+ /** Text of a message for the transcript-integrity guard — flattens the two
440
+ * content shapes so a user message can be matched across the display and
441
+ * canonical transcripts regardless of how each stored it. */
442
+ const messageText = (m: Message): string =>
443
+ typeof m.content === "string"
444
+ ? m.content
445
+ : Array.isArray(m.content)
446
+ ? m.content.map((p) => (p as { text?: string }).text ?? "").join("")
447
+ : "";
448
+
344
449
  // ── Turn metadata persistence ──
345
450
 
346
451
  export const applyTurnMetadata = (
@@ -373,6 +478,31 @@ export const applyTurnMetadata = (
373
478
  conv._harnessMessages = conv.messages;
374
479
  }
375
480
 
481
+ // Invariant guard: the model-facing transcript must not drop the current
482
+ // turn's user message. A resume that reconstructed `_harnessMessages`
483
+ // incorrectly silently lost it (the display transcript kept it, the model
484
+ // didn't), so the agent second-guessed an approval it never saw. This is the
485
+ // one choke point every finalize flows through — assert the latest user
486
+ // message survived into canonical, and log loudly if not. Matched by content
487
+ // (robust across the display/canonical shapes) and skipped when compaction
488
+ // has legitimately summarized history. Log-only; never throws.
489
+ if (isMessageArray(conv._harnessMessages) && conv._harnessMessages.length > 0) {
490
+ const canonical = conv._harnessMessages;
491
+ const summarized = canonical.some((m) => m.metadata?.isCompactionSummary);
492
+ const lastUser = [...conv.messages].reverse().find((m) => m.role === "user");
493
+ const lastUserText = lastUser ? messageText(lastUser).trim() : "";
494
+ if (!summarized && lastUserText) {
495
+ const present = canonical.some((m) => m.role === "user" && messageText(m).trim() === lastUserText);
496
+ if (!present) {
497
+ console.error(
498
+ `[transcript-guard] conversation ${conv.conversationId}: model-facing transcript ` +
499
+ `is missing the latest user message — it diverged from the display transcript. ` +
500
+ `This is a resume/finalize message-assembly bug; the model will not see that turn's input.`,
501
+ );
502
+ }
503
+ }
504
+ }
505
+
376
506
  if (meta.toolResultArchive !== undefined) {
377
507
  conv._toolResultArchive = meta.toolResultArchive;
378
508
  }
@@ -0,0 +1,156 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { Message } from "@poncho-ai/sdk";
3
+ import {
4
+ assembleCheckpointMessages,
5
+ buildToolResultMessage,
6
+ buildResumeCheckpoints,
7
+ applyTurnMetadata,
8
+ type StoredApproval,
9
+ } from "../src/orchestrator/index.js";
10
+ import type { Conversation } from "../src/state.js";
11
+
12
+ const conv = (overrides: Partial<Conversation> & Pick<Conversation, "messages">): Conversation => ({
13
+ conversationId: "conv_test",
14
+ title: "test",
15
+ ownerId: "owner",
16
+ tenantId: null,
17
+ createdAt: Date.now(),
18
+ updatedAt: Date.now(),
19
+ ...overrides,
20
+ });
21
+
22
+ // An assistant tool-call message is JSON `{ text, tool_calls: [...] }` in the
23
+ // canonical transcript.
24
+ const assistantToolCall = (text: string, calls: Array<{ id: string; name: string }>): Message => ({
25
+ role: "assistant",
26
+ content: JSON.stringify({ text, tool_calls: calls.map((c) => ({ ...c, input: {} })) }),
27
+ });
28
+
29
+ const meta = (harnessMessages?: Message[]) => ({
30
+ latestRunId: "",
31
+ contextTokens: 0,
32
+ contextWindow: 0,
33
+ harnessMessages,
34
+ });
35
+
36
+ describe("assembleCheckpointMessages", () => {
37
+ it("initial checkpoint: prior history (sliced by base) + delta", () => {
38
+ const prior: Message[] = [
39
+ { role: "user", content: "hi" },
40
+ { role: "assistant", content: "hello" },
41
+ ];
42
+ // Task-bearing turn: the delta carries the turn's user message + the
43
+ // assistant preamble/tool-call. base = prior length.
44
+ const delta: Message[] = [
45
+ { role: "user", content: "yeah those are a lot better" },
46
+ assistantToolCall("I found the two PE events…", [{ id: "call_1", name: "update_event" }]),
47
+ ];
48
+ const checkpoint = { baseMessageCount: prior.length, checkpointMessages: delta } as StoredApproval;
49
+ const out = assembleCheckpointMessages(conv({ messages: prior }), checkpoint);
50
+
51
+ expect(out).toHaveLength(4);
52
+ expect(out.some((m) => m.role === "user" && m.content === "yeah those are a lot better")).toBe(true);
53
+ expect(out.some((m) => typeof m.content === "string" && m.content.includes("I found the two PE events"))).toBe(true);
54
+ });
55
+
56
+ it("resume convention: base 0 + full canonical returns the full canonical verbatim", () => {
57
+ const full: Message[] = [
58
+ { role: "user", content: "hi" },
59
+ { role: "user", content: "approve this" },
60
+ assistantToolCall("preamble", [{ id: "call_1", name: "t" }]),
61
+ ];
62
+ const checkpoint = { baseMessageCount: 0, checkpointMessages: full } as StoredApproval;
63
+ // Display messages are irrelevant when base is 0 — the reconstruction must
64
+ // NOT depend on them (this is what the old PonchOS hand-merge got wrong).
65
+ const out = assembleCheckpointMessages(conv({ messages: [{ role: "user", content: "unrelated display" }] }), checkpoint);
66
+ expect(out).toEqual(full);
67
+ });
68
+ });
69
+
70
+ describe("buildToolResultMessage", () => {
71
+ const asst = assistantToolCall("", [{ id: "call_1", name: "update_event" }]);
72
+
73
+ it("pairs a provided result by callId", () => {
74
+ const msg = buildToolResultMessage(asst, [{ callId: "call_1", toolName: "update_event", result: { ok: true } }]);
75
+ expect(msg?.role).toBe("tool");
76
+ expect(msg?.content).toContain("call_1");
77
+ expect(msg?.content).toContain("ok");
78
+ });
79
+
80
+ it("emits the deferred-error marker when a result is missing", () => {
81
+ const msg = buildToolResultMessage(asst, []);
82
+ expect(msg?.content).toContain("Tool execution deferred");
83
+ });
84
+
85
+ it("returns undefined for a non-assistant / non-tool-call message", () => {
86
+ expect(buildToolResultMessage({ role: "user", content: "hi" }, [])).toBeUndefined();
87
+ expect(buildToolResultMessage({ role: "assistant", content: "plain text" }, [])).toBeUndefined();
88
+ });
89
+ });
90
+
91
+ describe("buildResumeCheckpoints + round-trip (the regression)", () => {
92
+ it("stores full canonical (base 0) so the NEXT resume reconstructs without dropping the user turn", () => {
93
+ // The continuation ran with this full canonical (incl. the turn's user
94
+ // message + preamble) and appended a tool result.
95
+ const fullWithResults: Message[] = [
96
+ { role: "user", content: "hi" },
97
+ { role: "assistant", content: "hello" },
98
+ { role: "user", content: "approve this" },
99
+ assistantToolCall("preamble text", [{ id: "call_1", name: "t1" }]),
100
+ { role: "tool", content: JSON.stringify([{ type: "tool_result", tool_use_id: "call_1", content: "done" }]) },
101
+ ];
102
+ // It then gated again on a new tool (the re-checkpoint event delta).
103
+ const delta2: Message[] = [assistantToolCall("", [{ id: "call_2", name: "t2" }])];
104
+ const stored = buildResumeCheckpoints({
105
+ priorMessages: fullWithResults,
106
+ checkpointEvent: {
107
+ approvals: [{ approvalId: "a2", tool: "t2", toolCallId: "call_2", input: {} }],
108
+ checkpointMessages: delta2,
109
+ pendingToolCalls: [{ id: "call_2", name: "t2", input: {} }],
110
+ },
111
+ runId: "run_2",
112
+ });
113
+
114
+ expect(stored[0]!.baseMessageCount).toBe(0);
115
+ expect(stored[0]!.checkpointMessages).toEqual([...fullWithResults, ...delta2]);
116
+
117
+ // Next resume reconstructs from the stored checkpoint — display is
118
+ // deliberately unrelated to prove no dependence on it.
119
+ const reconstructed = assembleCheckpointMessages(
120
+ conv({ messages: [{ role: "user", content: "unrelated" }], pendingApprovals: stored }),
121
+ stored[0]!,
122
+ );
123
+ // The user turn + preamble that PonchOS used to drop MUST survive.
124
+ expect(reconstructed.some((m) => m.role === "user" && m.content === "approve this")).toBe(true);
125
+ expect(reconstructed.some((m) => typeof m.content === "string" && m.content.includes("preamble text"))).toBe(true);
126
+ });
127
+ });
128
+
129
+ describe("applyTurnMetadata transcript-integrity guard", () => {
130
+ const latestUser: Message = { role: "user", content: "the latest question", metadata: { id: "u_latest" } };
131
+
132
+ it("logs when the latest user message is missing from the canonical transcript", () => {
133
+ const spy = vi.spyOn(console, "error").mockImplementation(() => {});
134
+ const c = conv({ messages: [{ role: "assistant", content: "earlier" }, latestUser] });
135
+ // Canonical is missing "the latest question" — the exact divergence.
136
+ applyTurnMetadata(c, meta([{ role: "user", content: "hi" }, { role: "assistant", content: "x" }]));
137
+ expect(spy).toHaveBeenCalledWith(expect.stringContaining("[transcript-guard]"));
138
+ spy.mockRestore();
139
+ });
140
+
141
+ it("stays silent when the latest user message is present in canonical", () => {
142
+ const spy = vi.spyOn(console, "error").mockImplementation(() => {});
143
+ const c = conv({ messages: [latestUser] });
144
+ applyTurnMetadata(c, meta([{ role: "user", content: "the latest question" }, { role: "assistant", content: "reply" }]));
145
+ expect(spy).not.toHaveBeenCalled();
146
+ spy.mockRestore();
147
+ });
148
+
149
+ it("skips the check when canonical was legitimately compacted", () => {
150
+ const spy = vi.spyOn(console, "error").mockImplementation(() => {});
151
+ const c = conv({ messages: [latestUser] });
152
+ applyTurnMetadata(c, meta([{ role: "assistant", content: "summary", metadata: { isCompactionSummary: true } }]));
153
+ expect(spy).not.toHaveBeenCalled();
154
+ spy.mockRestore();
155
+ });
156
+ });