Skip to content
Merged
26 changes: 23 additions & 3 deletions crates/tinyflows-bus/src/schedule/identity_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ fn channel_job_origin_carries_optional_session_identity_without_changing_legacy_
history_key: "telegram:chat-1".into(),
sender: None,
thread_id: None,
session_identity: Some(Box::new(channel_identity())),
channel_session: Some(Box::new(channel_identity())),
};
let value = serde_json::to_value(&origin).unwrap();
assert_eq!(value["kind"], "channel");
assert_eq!(value["session_identity"]["session_key"], "session-1");
assert_eq!(value["channel_session"]["session_key"], "session-1");
assert_eq!(serde_json::from_value::<JobOrigin>(value).unwrap(), origin);

let legacy = json!({
Expand All @@ -77,8 +77,28 @@ fn channel_job_origin_carries_optional_session_identity_without_changing_legacy_
history_key: "discord:room-1".into(),
sender: None,
thread_id: None,
session_identity: None,
channel_session: None,
}
);
assert_eq!(serde_json::to_value(parsed).unwrap(), legacy);
}

#[test]
fn channel_origin_accepts_session_identity_wire_alias() {
let origin: JobOrigin = serde_json::from_value(json!({
"kind": "channel",
"channel": "telegram",
"reply_target": "chat-1",
"history_key": "telegram:chat-1",
"session_identity": channel_identity(),
}))
.unwrap();

assert!(matches!(
origin,
JobOrigin::Channel {
channel_session: Some(_),
..
}
));
}
4 changes: 2 additions & 2 deletions crates/tinyflows-bus/src/schedule/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ pub const CALLBACK_INTERFACE: &str = "ai.tinyhumans.tinyflows.ScheduleHost";
/// send/terminal delivery phases. 6 adds prepared handles and host-owned
/// lifecycle observation for detached manual workers. 7 adds `TickSchedulerAt`,
/// a manual pass at a host-supplied calendar instant, so a host can step a
/// schedule forward instead of waiting for it. 8 adds optional validated
/// channel-session identity to channel job origins.
/// schedule forward instead of waiting for it. 8 adds an optional opaque,
/// host-validated durable channel-session identity to channel job origins.
pub const CONTRACT_VERSION: u32 = 8;
/// Exact advertised scheduler member set.
pub const METHODS: &[&str] = &[
Expand Down
14 changes: 10 additions & 4 deletions crates/tinyflows-bus/src/schedule/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,16 @@ pub enum JobOrigin {
/// Channel-side thread or topic within the chat.
#[serde(default, skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
/// Validated durable session identity supplied by the channel host.
/// Older origins omit it and retain their existing wire shape.
#[serde(default, skip_serializing_if = "Option::is_none")]
session_identity: Option<Box<ChannelSessionIdentity>>,
/// Opaque, host-derived durable session binding for this channel
/// origin. TinyFlows stores and forwards it without interpreting it;
/// the host validates it against trusted context before using it.
/// `session_identity` is accepted as a wire alias for compatibility.
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "session_identity"
)]
channel_session: Option<Box<ChannelSessionIdentity>>,
},
}

Expand Down
19 changes: 17 additions & 2 deletions crates/tinyflows-bus/src/schedule/types_tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use crate::schedule::ChannelConversationIdentity;
use chrono::TimeZone;
use serde_json::json;

Expand Down Expand Up @@ -317,7 +318,21 @@ fn job_origin_channel_roundtrips_with_and_without_optionals() {
history_key: "telegram:chat-42".into(),
sender: Some("alice".into()),
thread_id: Some("topic-7".into()),
session_identity: None,
channel_session: Some(Box::new(ChannelSessionIdentity {
tenant_key: "profile:v1:local".into(),
agent_id: "telegram-agent".into(),
session_key: "main:telegram:account-a:group:room-42:topic:topic-7".into(),
provider: "telegram".into(),
account_id: Some("account-a".into()),
conversation: ChannelConversationIdentity {
kind: "group".into(),
id: "room-42".into(),
scope_id: Some("workspace-3".into()),
parent_id: Some("parent-9".into()),
thread_id: Some("topic-7".into()),
topic_id: Some("topic-7".into()),
},
})),
};
let v = serde_json::to_value(&full).unwrap();
assert_eq!(v["kind"], "channel");
Expand All @@ -341,7 +356,7 @@ fn job_origin_channel_roundtrips_with_and_without_optionals() {
history_key: "discord:c1".into(),
sender: None,
thread_id: None,
session_identity: None,
channel_session: None,
}
);
assert_eq!(serde_json::to_value(&parsed).unwrap(), minimal);
Expand Down
8 changes: 4 additions & 4 deletions crates/tinyflows-bus/src/schedule/wire_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,22 +123,22 @@ fn origin_wire_bytes() {
history_key: "telegram:42".into(),
sender: Some("alice".into()),
thread_id: Some("7".into()),
session_identity: None,
channel_session: None,
},
);
}

#[test]
fn channel_origin_with_session_identity_wire_bytes() {
fn channel_origin_with_channel_session_wire_bytes() {
assert_bytes(
r#"{"kind":"channel","channel":"telegram","reply_target":"42","history_key":"telegram:42","session_identity":{"tenant_key":"tenant-1","agent_id":"agent-1","session_key":"session-1","provider":"telegram","conversation":{"kind":"group","id":"chat-1","thread_id":"thread-1"}}}"#,
r#"{"kind":"channel","channel":"telegram","reply_target":"42","history_key":"telegram:42","channel_session":{"tenant_key":"tenant-1","agent_id":"agent-1","session_key":"session-1","provider":"telegram","conversation":{"kind":"group","id":"chat-1","thread_id":"thread-1"}}}"#,
&JobOrigin::Channel {
channel: "telegram".into(),
reply_target: "42".into(),
history_key: "telegram:42".into(),
sender: None,
thread_id: None,
session_identity: Some(Box::new(ChannelSessionIdentity {
channel_session: Some(Box::new(ChannelSessionIdentity {
tenant_key: "tenant-1".into(),
agent_id: "agent-1".into(),
session_key: "session-1".into(),
Expand Down
2 changes: 1 addition & 1 deletion crates/tinyflows-drivers/src/schedule/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub(super) fn channel_origin() -> JobOrigin {
history_key: "telegram:chat-42".into(),
sender: Some("alice".into()),
thread_id: None,
session_identity: None,
channel_session: None,
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/tinyflows-schedule/src/validation_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ fn origin_requires_conversation_and_announce_can_reply_to_its_asker() {
history_key: "conversation".into(),
sender: None,
thread_id: None,
session_identity: None,
channel_session: None,
};
assert!(validate_delivery(&delivery, Some(&origin), None).is_ok());
let announce = DeliveryConfig {
Expand Down
2 changes: 1 addition & 1 deletion crates/tinyflows-schedule/src/wire_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ fn origin_wire_bytes() {
history_key: "telegram:42".into(),
sender: Some("alice".into()),
thread_id: Some("7".into()),
session_identity: None,
channel_session: None,
},
);
}
Expand Down
6 changes: 3 additions & 3 deletions crates/tinyflows-storage/src/schema/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ impl Schema {
}
}
for row in &page.items {
if (source != step.item || row.meta.schema < step.to)
&& (source != step.item
|| !output_ids.contains(&(step.item.to_owned(), row.id.clone())))
if source != step.item
|| (row.meta.schema < step.to
&& !output_ids.contains(&(step.item.to_owned(), row.id.clone())))
Comment on lines +138 to +140

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority high tests likely

Keep the migration from deleting rows no step touched

The original condition deleted only rows that were (a) in a different collection than the step's item, or (b) in the step's own collection, still below `step.to`, and not among this batch's outputs — i.e. rows the step left behind. The rewritten form first disjunct is `source != step.item`, with no `row.meta.schema < step.to` requirement, so when a step reads from another collection every row in the page is now deleted unconditionally after the outputs are written. Concretely: any step with `step.source = Some(other)` (the `if source != step.item` branch at `let source = step.source.unwrap_or(step.item);` exists precisely for this) wipes the entire source collection as it pages through it, even rows the step did not touch or preserve via `output_ids`. Restore the previous structure, or at minimum require `row.meta.schema < step.to` on the `source != step.item` branch as well.

Suggested change
if source != step.item
|| (row.meta.schema < step.to
&& !output_ids.contains(&(step.item.to_owned(), row.id.clone())))
if (source != step.item || row.meta.schema < step.to)
&& (source != step.item
|| !output_ids.contains(&(step.item.to_owned(), row.id.clone())))

[RULE] unnecessary-delete ·

{
match docs
.delete(source, &row.id, Precondition::Version(row.version))
Expand Down
1 change: 1 addition & 0 deletions crates/tinyflows/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ use routing::{
};

mod api;
#[cfg(any(test, feature = "mock"))]
pub(crate) use api::run_preflight_with_observer;
pub(crate) use api::run_sub_workflow;
pub use api::{
Expand Down
1 change: 1 addition & 0 deletions crates/tinyflows/src/engine/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub async fn run_with_observer(

/// Observe a preflight child with the traversal's inherited depth counters.
/// The overlay takes precedence over any cap on the child's own trigger.
#[cfg(any(test, feature = "mock"))]
pub(crate) async fn run_preflight_with_observer(
Comment on lines +70 to 71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority critical tests confident

Do not gate run_preflight_with_observer behind test/mock

`crates/tinyflows/src/preflight.rs` unconditionally calls `crate::engine::run_preflight_with_observer(&compiled, input, &caps, &observer_dyn, nesting)` — that call site carries no `#[cfg]`. With this gate the function (and the re-export in `engine.rs`) only exists when `cfg(test)` or `feature = "mock"` is set, so a default `cargo build` of the `tinyflows` crate fails with `E0425: cannot find function run_preflight_with_observer in module crate::engine`. `mock` is `default = []`, so the production build is exactly the one that loses the symbol. Either drop the gate or gate the `preflight.rs` call site identically.

Suggested change
#[cfg(any(test, feature = "mock"))]
pub(crate) async fn run_preflight_with_observer(
pub(crate) async fn run_preflight_with_observer(

[RULE] cfg-gated-symbol-used-in-production ·

workflow: &CompiledWorkflow,
input: RunInput,
Expand Down
Loading