@yeaft/webchat-agent 1.0.573 → 1.0.575

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.573",
3
+ "version": "1.0.575",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1962,9 +1962,12 @@ export class ConversationStore {
1962
1962
  * copied persisted message are remapped; external/client/tool identities are
1963
1963
  * intentionally preserved.
1964
1964
  *
1965
+ * @param {string} sourceSessionId
1966
+ * @param {string} targetSessionId
1967
+ * @param {{throughTurnId?: string}} [options]
1965
1968
  * @returns {{ copiedCount: number, idMap: Map<string, string> }}
1966
1969
  */
1967
- copySession(sourceSessionId, targetSessionId) {
1970
+ copySession(sourceSessionId, targetSessionId, options = {}) {
1968
1971
  if (!sourceSessionId || !targetSessionId || sourceSessionId === targetSessionId) {
1969
1972
  return { copiedCount: 0, idMap: new Map() };
1970
1973
  }
@@ -1986,7 +1989,44 @@ export class ConversationStore {
1986
1989
  if (row?.sessionId !== sourceSessionId || typeof row.id !== 'string' || !row.id) continue;
1987
1990
  rowsById.set(row.id, row);
1988
1991
  }
1989
- const rows = [...rowsById.values()].sort(compareMessagesBySeq);
1992
+ let rows = [...rowsById.values()].sort(compareMessagesBySeq);
1993
+ if (Object.prototype.hasOwnProperty.call(options, 'throughTurnId')) {
1994
+ const throughTurnId = typeof options.throughTurnId === 'string'
1995
+ ? options.throughTurnId.trim()
1996
+ : '';
1997
+ if (!throughTurnId) {
1998
+ const error = new Error('throughTurnId must be a nonempty string');
1999
+ error.code = 'invalid_fork_boundary';
2000
+ throw error;
2001
+ }
2002
+
2003
+ const matchingAssistantRows = rows.filter(row => row?.role === 'assistant' && row.turnId === throughTurnId);
2004
+ if (matchingAssistantRows.length === 0) {
2005
+ const error = new Error(`Assistant turn not found: ${throughTurnId}`);
2006
+ error.code = 'fork_boundary_not_found';
2007
+ throw error;
2008
+ }
2009
+ const speakers = new Set(matchingAssistantRows.map(row => row.speakerVpId || row.vpId || '').filter(Boolean));
2010
+ if (speakers.size > 1) {
2011
+ const error = new Error(`Assistant turn boundary is ambiguous: ${throughTurnId}`);
2012
+ error.code = 'ambiguous_fork_boundary';
2013
+ throw error;
2014
+ }
2015
+ const endpoint = rows.reduce((last, row, index) => row?.turnId === throughTurnId ? index : last, -1);
2016
+ const prefix = rows.slice(0, endpoint + 1);
2017
+ const sanitized = pairSanitize(prefix);
2018
+ const pairShape = row => ({
2019
+ id: row?.id,
2020
+ toolCalls: Array.isArray(row?.toolCalls) ? row.toolCalls.map(call => call?.id) : undefined,
2021
+ });
2022
+ if (sanitized.length !== prefix.length
2023
+ || sanitized.some((row, index) => JSON.stringify(pairShape(row)) !== JSON.stringify(pairShape(prefix[index])))) {
2024
+ const error = new Error(`Assistant turn boundary would split provider tool history: ${throughTurnId}`);
2025
+ error.code = 'incomplete_fork_boundary';
2026
+ throw error;
2027
+ }
2028
+ rows = prefix;
2029
+ }
1990
2030
  if (rows.length === 0) return { copiedCount: 0, idMap: new Map() };
1991
2031
 
1992
2032
  const firstSeq = this.#reserveSeqRange(rows.length);
@@ -641,11 +641,17 @@ export function copySession(yeaftDir, sourceSessionId, options = {}) {
641
641
  }
642
642
 
643
643
  const transcript = new ConversationStore(sourceYeaftDir);
644
- const { copiedCount } = transcript.copySession(sourceSessionId, copied.id);
644
+ const transcriptOptions = Object.prototype.hasOwnProperty.call(options, 'throughTurnId')
645
+ ? { throughTurnId: options.throughTurnId }
646
+ : {};
647
+ const { copiedCount } = transcript.copySession(sourceSessionId, copied.id, transcriptOptions);
645
648
  return { ...requireSessionMeta(sourceYeaftDir, copied.id), copiedMessageCount: copiedCount };
646
649
  } catch (error) {
647
650
  // A partial clone must never appear as a successful copy.
648
651
  deleteSession(sourceYeaftDir, copied.id, options);
652
+ if (['invalid_fork_boundary', 'fork_boundary_not_found', 'ambiguous_fork_boundary', 'incomplete_fork_boundary'].includes(error?.code)) {
653
+ throw new SessionCrudError(error.code, sourceSessionId, error.message);
654
+ }
649
655
  throw error;
650
656
  }
651
657
  }
@@ -3663,6 +3663,9 @@ export function handleYeaftCopySession(msg) {
3663
3663
  const session = copySession(yeaftDir, sessionId, {
3664
3664
  ...configuredVpPaths(),
3665
3665
  name: msg && msg.name,
3666
+ ...(msg && Object.prototype.hasOwnProperty.call(msg, 'throughTurnId')
3667
+ ? { throughTurnId: msg.throughTurnId }
3668
+ : {}),
3666
3669
  });
3667
3670
  recordAgentSessionCreated();
3668
3671
  session.config = loadSessionConfig(yeaftDir, session.id);