spine-framework 0.4.0 → 0.4.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.
@@ -4256,7 +4256,23 @@ export async function runAgent(
4256
4256
  const threadVisibility = thread?.visibility || 'external'
4257
4257
  let state = getResearchState(thread)
4258
4258
  if (userMessage?.trim()) {
4259
- await saveMessage(threadId, userMessage.trim(), 'human', null, ctx, replyTypeSlug, threadVisibility)
4259
+ // Idempotent insert: the caller (ticket creation, sendMessage) often
4260
+ // persists the inbound row itself. When the thread's latest inbound
4261
+ // already carries this text the row exists — skip the insert so a
4262
+ // re-fired kick/step can never double-post the question. New-turn
4263
+ // semantics below (episode reset, user_question) are unaffected.
4264
+ const { data: lastInboundRows } = await adminDb
4265
+ .from('messages')
4266
+ .select('content')
4267
+ .eq('thread_id', threadId)
4268
+ .eq('direction', 'inbound')
4269
+ .order('sequence', { ascending: false })
4270
+ .order('created_at', { ascending: false })
4271
+ .limit(1)
4272
+ const lastInbound = Array.isArray(lastInboundRows) ? lastInboundRows[0] : lastInboundRows
4273
+ if ((lastInbound?.content || '').trim() !== userMessage.trim()) {
4274
+ await saveMessage(threadId, userMessage.trim(), 'human', null, ctx, replyTypeSlug, threadVisibility)
4275
+ }
4260
4276
  if (!state.user_question) state.user_question = userMessage.trim()
4261
4277
  }
4262
4278
  if (!userMessage?.trim() && !state.user_question) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Multi-tenant, modular application platform for modern SaaS systems",
5
5
  "type": "module",
6
6
  "bin": {