@poncho-ai/harness 0.59.0 → 0.59.2

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.59.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.59.2 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 567.16 KB
12
11
  ESM dist/isolate-F2PPSUL6.js 53.82 KB
13
- ESM ⚡️ Build success in 256ms
12
+ ESM dist/index.js 567.57 KB
13
+ ESM ⚡️ Build success in 251ms
14
14
  DTS Build start
15
- DTS ⚡️ Build success in 8437ms
15
+ DTS ⚡️ Build success in 7447ms
16
16
  DTS dist/index.d.ts 104.68 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.59.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [`ac0faae`](https://github.com/cesr/poncho-ai/commit/ac0faae54365afda5ef518b0a306a8cde5978ca8) Thanks [@cesr](https://github.com/cesr)! - conversations.rename now does a targeted title-column UPDATE instead of a
8
+ whole-row get→mutate→update. The read-modify-write raced a streaming turn's
9
+ per-step draft persist: a rename landing mid-run wrote the stale blob back
10
+ and silently reverted the turn's persisted progress.
11
+
12
+ ## 0.59.1
13
+
14
+ ### Patch Changes
15
+
16
+ - [`299f574`](https://github.com/cesr/poncho-ai/commit/299f574a2f2f0d4873f42bbcffdf604e9cc4c29c) Thanks [@cesr](https://github.com/cesr)! - Mark in-flight assistant drafts with `metadata.incomplete = true`.
17
+
18
+ The orchestrator's per-step draft persist (`persistDraft`) and the
19
+ approval/device checkpoint and continuation writes now stamp the trailing
20
+ assistant message `metadata.incomplete = true`; the three terminal writes
21
+ (normal finalize, cancelled, errored) clear it. This lets a consumer that
22
+ reconciles a persisted snapshot against a live event stream (e.g. a
23
+ WebSocket layer) strip the in-flight draft from the authoritative snapshot
24
+ and rebuild that turn from the event log instead — so the snapshot and the
25
+ replayed events never both carry the in-flight turn, eliminating
26
+ reconnect-time duplication. Additive + backwards-compatible: consumers that
27
+ ignore the flag are unaffected.
28
+
29
+ - Updated dependencies [[`299f574`](https://github.com/cesr/poncho-ai/commit/299f574a2f2f0d4873f42bbcffdf604e9cc4c29c)]:
30
+ - @poncho-ai/sdk@1.15.1
31
+
3
32
  ## 0.59.0
4
33
 
5
34
  ### Minor Changes
package/dist/index.js CHANGED
@@ -3966,11 +3966,15 @@ var SqlStorageEngine = class {
3966
3966
  );
3967
3967
  },
3968
3968
  rename: async (conversationId, title) => {
3969
- const conv = await this.conversations.get(conversationId);
3970
- if (!conv) return void 0;
3971
- conv.title = normalizeTitle2(title);
3972
- await this.conversations.update(conv);
3973
- return conv;
3969
+ const normalized = normalizeTitle2(title);
3970
+ await this.executor.run(
3971
+ rewrite(
3972
+ `UPDATE conversations SET title = $1, updated_at = $2 WHERE id = $3`,
3973
+ this.dialect
3974
+ ),
3975
+ [normalized, (/* @__PURE__ */ new Date()).toISOString(), conversationId]
3976
+ );
3977
+ return this.conversations.get(conversationId);
3974
3978
  },
3975
3979
  delete: async (conversationId) => {
3976
3980
  const row = await this.executor.get(
@@ -14477,7 +14481,7 @@ var runConversationTurn = async (opts) => {
14477
14481
  let runContinuationMessages;
14478
14482
  let cancelHarnessMessages;
14479
14483
  let checkpointedRun = false;
14480
- const buildMessages = () => {
14484
+ const buildMessages = (incomplete = true) => {
14481
14485
  const draftSections = cloneSections(draft.sections);
14482
14486
  if (draft.currentTools.length > 0) {
14483
14487
  draftSections.push({ type: "tools", content: [...draft.currentTools] });
@@ -14496,10 +14500,15 @@ var runConversationTurn = async (opts) => {
14496
14500
  {
14497
14501
  role: "assistant",
14498
14502
  content: draft.assistantResponse,
14499
- metadata: buildAssistantMetadata(draft, draftSections, {
14500
- id: assistantId,
14501
- timestamp: turnTimestamp
14502
- })
14503
+ metadata: {
14504
+ ...buildAssistantMetadata(draft, draftSections, {
14505
+ id: assistantId,
14506
+ timestamp: turnTimestamp
14507
+ }),
14508
+ // Only stamp the flag when true; finalize omits it so completed
14509
+ // assistants stay clean (no `incomplete: false` noise on the row).
14510
+ ...incomplete ? { incomplete: true } : {}
14511
+ }
14503
14512
  }
14504
14513
  ];
14505
14514
  };
@@ -14721,7 +14730,7 @@ var runConversationTurn = async (opts) => {
14721
14730
  flushTurnDraft(draft);
14722
14731
  latestRunId = execution.latestRunId || latestRunId;
14723
14732
  if (!checkpointedRun && !runContinuationMessages) {
14724
- conversation.messages = buildMessages();
14733
+ conversation.messages = buildMessages(false);
14725
14734
  applyTurnMetadata(
14726
14735
  conversation,
14727
14736
  {
@@ -14779,7 +14788,7 @@ var runConversationTurn = async (opts) => {
14779
14788
  const aborted = opts.abortSignal?.aborted === true;
14780
14789
  if (aborted || runCancelled) {
14781
14790
  if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
14782
- conversation.messages = buildMessages();
14791
+ conversation.messages = buildMessages(false);
14783
14792
  applyTurnMetadata(
14784
14793
  conversation,
14785
14794
  {
@@ -14828,7 +14837,7 @@ var runConversationTurn = async (opts) => {
14828
14837
  }
14829
14838
  }
14830
14839
  if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
14831
- conversation.messages = buildMessages();
14840
+ conversation.messages = buildMessages(false);
14832
14841
  conversation.updatedAt = Date.now();
14833
14842
  await opts.conversationStore.update(conversation);
14834
14843
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.59.0",
3
+ "version": "0.59.2",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "mustache": "^4.2.0",
35
35
  "yaml": "^2.4.0",
36
36
  "zod": "^3.22.0",
37
- "@poncho-ai/sdk": "1.15.0"
37
+ "@poncho-ai/sdk": "1.15.1"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "esbuild": ">=0.17.0",
@@ -157,7 +157,15 @@ export const runConversationTurn = async (
157
157
  let cancelHarnessMessages: Message[] | undefined;
158
158
  let checkpointedRun = false;
159
159
 
160
- const buildMessages = (): Message[] => {
160
+ // `incomplete: true` (the default) marks the trailing assistant message as
161
+ // an in-flight DRAFT — content for a turn that hasn't finished. A consumer
162
+ // (e.g. PonchOS's WS snapshot) uses this to strip the draft from the
163
+ // authoritative snapshot: the in-flight turn is delivered by the event
164
+ // stream instead, so the snapshot and the event log never both carry it
165
+ // (no reconnect duplication). The three TERMINAL writes (normal finalize,
166
+ // cancelled, errored) pass `incomplete: false` — at that point the turn is
167
+ // done and the assistant message is authoritative.
168
+ const buildMessages = (incomplete = true): Message[] => {
161
169
  const draftSections = cloneSections(draft.sections);
162
170
  if (draft.currentTools.length > 0) {
163
171
  draftSections.push({ type: "tools", content: [...draft.currentTools] });
@@ -179,10 +187,15 @@ export const runConversationTurn = async (
179
187
  {
180
188
  role: "assistant" as const,
181
189
  content: draft.assistantResponse,
182
- metadata: buildAssistantMetadata(draft, draftSections, {
183
- id: assistantId,
184
- timestamp: turnTimestamp,
185
- }),
190
+ metadata: {
191
+ ...buildAssistantMetadata(draft, draftSections, {
192
+ id: assistantId,
193
+ timestamp: turnTimestamp,
194
+ }),
195
+ // Only stamp the flag when true; finalize omits it so completed
196
+ // assistants stay clean (no `incomplete: false` noise on the row).
197
+ ...(incomplete ? { incomplete: true } : {}),
198
+ },
186
199
  },
187
200
  ];
188
201
  };
@@ -442,7 +455,7 @@ export const runConversationTurn = async (
442
455
  latestRunId = execution.latestRunId || latestRunId;
443
456
 
444
457
  if (!checkpointedRun && !runContinuationMessages) {
445
- conversation.messages = buildMessages();
458
+ conversation.messages = buildMessages(false); // terminal: turn complete
446
459
  applyTurnMetadata(
447
460
  conversation,
448
461
  {
@@ -515,7 +528,7 @@ export const runConversationTurn = async (
515
528
  draft.toolTimeline.length > 0 ||
516
529
  draft.sections.length > 0
517
530
  ) {
518
- conversation.messages = buildMessages();
531
+ conversation.messages = buildMessages(false); // terminal: cancelled
519
532
  applyTurnMetadata(
520
533
  conversation,
521
534
  {
@@ -571,7 +584,7 @@ export const runConversationTurn = async (
571
584
  draft.toolTimeline.length > 0 ||
572
585
  draft.sections.length > 0
573
586
  ) {
574
- conversation.messages = buildMessages();
587
+ conversation.messages = buildMessages(false); // terminal: errored
575
588
  conversation.updatedAt = Date.now();
576
589
  await opts.conversationStore.update(conversation);
577
590
  }
@@ -557,11 +557,21 @@ export abstract class SqlStorageEngine implements StorageEngine {
557
557
  conversationId: string,
558
558
  title: string,
559
559
  ): Promise<Conversation | undefined> => {
560
- const conv = await this.conversations.get(conversationId);
561
- if (!conv) return undefined;
562
- conv.title = normalizeTitle(title);
563
- await this.conversations.update(conv);
564
- return conv;
560
+ // Targeted column update — deliberately NOT get→mutate→update().
561
+ // The whole-row read-modify-write races a streaming turn's per-step
562
+ // draft persist: rename reads the row at T0, the turn persists step
563
+ // N's draft at T1, rename writes T0's stale blob back at T2 and
564
+ // silently reverts the turn's progress. Title lives in its own
565
+ // column, so touch only that (+ updated_at for sidebar ordering).
566
+ const normalized = normalizeTitle(title);
567
+ await this.executor.run(
568
+ rewrite(
569
+ `UPDATE conversations SET title = $1, updated_at = $2 WHERE id = $3`,
570
+ this.dialect,
571
+ ),
572
+ [normalized, new Date().toISOString(), conversationId],
573
+ );
574
+ return this.conversations.get(conversationId);
565
575
  },
566
576
 
567
577
  delete: async (conversationId: string): Promise<boolean> => {