chalk-remote-call-python 1.10.1__tar.gz → 1.10.2__tar.gz

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.
Files changed (68) hide show
  1. {chalk_remote_call_python-1.10.1/chalk_remote_call_python.egg-info → chalk_remote_call_python-1.10.2}/PKG-INFO +1 -1
  2. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/Cargo.lock +1 -1
  3. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/Cargo.toml +1 -1
  4. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/self_consumer.rs +192 -19
  5. chalk_remote_call_python-1.10.2/chalk_remote_call/_version.py +1 -0
  6. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2/chalk_remote_call_python.egg-info}/PKG-INFO +1 -1
  7. chalk_remote_call_python-1.10.1/chalk_remote_call/_version.py +0 -1
  8. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/MANIFEST.in +0 -0
  9. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/README.md +0 -0
  10. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/Cargo.toml +0 -0
  11. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/Cargo.toml +0 -0
  12. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.auth.v1.rs +0 -0
  13. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.common.v1.rs +0 -0
  14. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.rs +0 -0
  15. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.tonic.rs +0 -0
  16. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.utils.v1.rs +0 -0
  17. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/descriptor.bin +0 -0
  18. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/lib.rs +0 -0
  19. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/async_service.rs +0 -0
  20. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/coalesce.rs +0 -0
  21. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/lib.rs +0 -0
  22. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/metrics.rs +0 -0
  23. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/python_bridge.rs +0 -0
  24. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/server.rs +0 -0
  25. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/service.rs +0 -0
  26. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/rust-toolchain.toml +0 -0
  27. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/__init__.py +0 -0
  28. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/__main__.py +0 -0
  29. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/__init__.py +0 -0
  30. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/__init__.py +0 -0
  31. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/auth/__init__.py +0 -0
  32. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/auth/v1/__init__.py +0 -0
  33. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2.py +0 -0
  34. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2_grpc.py +0 -0
  35. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/common/__init__.py +0 -0
  36. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/common/v1/__init__.py +0 -0
  37. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2.py +0 -0
  38. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2_grpc.py +0 -0
  39. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/runtime/__init__.py +0 -0
  40. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/runtime/v1/__init__.py +0 -0
  41. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2.py +0 -0
  42. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2_grpc.py +0 -0
  43. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/__init__.py +0 -0
  44. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/v1/__init__.py +0 -0
  45. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2.py +0 -0
  46. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2_grpc.py +0 -0
  47. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2.py +0 -0
  48. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2_grpc.py +0 -0
  49. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2.py +0 -0
  50. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2_grpc.py +0 -0
  51. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_native.pyi +0 -0
  52. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/arrow_utils.py +0 -0
  53. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/cli.py +0 -0
  54. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/errors.py +0 -0
  55. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/handler_loader.py +0 -0
  56. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/input_transform.py +0 -0
  57. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/logging.py +0 -0
  58. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/server.py +0 -0
  59. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/servicer.py +0 -0
  60. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call/tracing.py +0 -0
  61. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call_python.egg-info/SOURCES.txt +0 -0
  62. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call_python.egg-info/dependency_links.txt +0 -0
  63. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call_python.egg-info/entry_points.txt +0 -0
  64. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call_python.egg-info/requires.txt +0 -0
  65. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/chalk_remote_call_python.egg-info/top_level.txt +0 -0
  66. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/pyproject.toml +0 -0
  67. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/setup.cfg +0 -0
  68. {chalk_remote_call_python-1.10.1 → chalk_remote_call_python-1.10.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chalk-remote-call-python
3
- Version: 1.10.1
3
+ Version: 1.10.2
4
4
  Summary: Chalk remote call Python runtime interface client
5
5
  Author: Chalk AI, Inc.
6
6
  Project-URL: Homepage, https://chalk.ai
@@ -511,7 +511,7 @@ dependencies = [
511
511
  [[package]]
512
512
  name = "chalk-queue"
513
513
  version = "0.0.0"
514
- source = "git+https://github.com/chalk-ai/chalk-private.git?rev=41458754e6916245a092ddc09562bcaf4dea373e#41458754e6916245a092ddc09562bcaf4dea373e"
514
+ source = "git+https://github.com/chalk-ai/chalk-private.git?rev=1460144ea659debbaee9dccadcd4d7dbf383ccf8#1460144ea659debbaee9dccadcd4d7dbf383ccf8"
515
515
  dependencies = [
516
516
  "base64 0.22.1",
517
517
  "redis",
@@ -37,6 +37,6 @@ futures-util = "0.3"
37
37
  # `[patch.crates-io]` here it builds against vanilla crates.io redis (it only
38
38
  # uses standard APIs). Metrics and StatsD share a revision so their client types agree.
39
39
  [target.'cfg(target_os = "linux")'.dependencies]
40
- chalk-queue = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "41458754e6916245a092ddc09562bcaf4dea373e" }
40
+ chalk-queue = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "1460144ea659debbaee9dccadcd4d7dbf383ccf8" }
41
41
  chalk_metrics = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "a0f8932ec10d3b8340af3e98cf63b800d60b4926" }
42
42
  chalk_statsd = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "a0f8932ec10d3b8340af3e98cf63b800d60b4926" }
@@ -39,7 +39,7 @@ use std::time::{Duration, Instant};
39
39
 
40
40
  use chalk_queue::semaphore::renew_many;
41
41
  use chalk_queue::stream_writer;
42
- use chalk_queue::types::QueuedCall;
42
+ use chalk_queue::types::{DeliveryAttempt, QueuedCall};
43
43
  use chalk_queue::{
44
44
  BoundFunctionQueue, DequeuedCall, FunctionQueue, QueueProtocol, QueueReceipt, SemaphoreLease,
45
45
  };
@@ -689,23 +689,81 @@ async fn run_one_call(
689
689
  flush_timeout: Duration,
690
690
  metrics_pipeline: Option<Arc<PublishingMetricsPipeline>>,
691
691
  ) {
692
+ let attempt = match input_queue.delivery_attempt(&delivery).await {
693
+ Ok(attempt) => attempt,
694
+ Err(error) => {
695
+ error!(call_id = %delivery.call.call_id, %error, "failed to resolve delivery attempt");
696
+ finish_delivery(
697
+ &input_queue,
698
+ &delivery.receipt,
699
+ false,
700
+ &delivery.call.call_id,
701
+ lease,
702
+ &live_calls,
703
+ )
704
+ .await;
705
+ return;
706
+ }
707
+ };
692
708
  let DequeuedCall {
693
709
  call: work,
694
710
  receipt,
695
711
  } = delivery;
696
712
  let call_id = work.call_id.clone();
713
+ let final_attempt = match attempt {
714
+ DeliveryAttempt::Ready {
715
+ attempt,
716
+ max_attempts,
717
+ } => {
718
+ info!(%call_id, attempt, max_attempts, "executing queued call attempt");
719
+ attempt == max_attempts
720
+ }
721
+ DeliveryAttempt::Exhausted {
722
+ attempts,
723
+ max_attempts,
724
+ } => {
725
+ let message = format!(
726
+ "retry attempts exhausted after {attempts} deliveries ({max_attempts} executions allowed)"
727
+ );
728
+ let completed = write_terminal_error(
729
+ &queue,
730
+ &mut conn,
731
+ &work,
732
+ "RetryAttemptsExhausted",
733
+ &message,
734
+ None,
735
+ )
736
+ .await
737
+ .is_ok();
738
+ finish_delivery(
739
+ &input_queue,
740
+ &receipt,
741
+ completed,
742
+ &call_id,
743
+ lease,
744
+ &live_calls,
745
+ )
746
+ .await;
747
+ return;
748
+ }
749
+ DeliveryAttempt::OwnershipLost => {
750
+ finish_delivery(&input_queue, &receipt, false, &call_id, lease, &live_calls).await;
751
+ return;
752
+ }
753
+ };
697
754
  let completed = match AssertUnwindSafe(execute_and_write(
698
755
  &handler,
699
756
  &queue,
700
757
  &mut conn,
701
- work,
758
+ work.clone(),
702
759
  flush_timeout,
760
+ final_attempt,
703
761
  metrics_pipeline.as_ref(),
704
762
  ))
705
763
  .catch_unwind()
706
764
  .await
707
765
  {
708
- Ok(Ok(())) => true,
766
+ Ok(Ok(completed)) => completed,
709
767
  Ok(Err(e)) => {
710
768
  // The call's result stream may be incomplete; the enqueue safety TTL
711
769
  // and the meta status are the backstop for a stuck poller.
@@ -723,7 +781,20 @@ async fn run_one_call(
723
781
  panic = %panic_message,
724
782
  "panic while executing/writing queued call"
725
783
  );
726
- false
784
+ if final_attempt {
785
+ write_terminal_error(
786
+ &queue,
787
+ &mut conn,
788
+ &work,
789
+ "RemoteFunctionPanic",
790
+ panic_message,
791
+ None,
792
+ )
793
+ .await
794
+ .is_ok()
795
+ } else {
796
+ false
797
+ }
727
798
  }
728
799
  };
729
800
 
@@ -777,21 +848,79 @@ async fn run_one_coalesced_call(
777
848
  live_calls: LiveCalls,
778
849
  metrics_pipeline: Option<Arc<PublishingMetricsPipeline>>,
779
850
  ) {
851
+ let attempt = match input_queue.delivery_attempt(&delivery).await {
852
+ Ok(attempt) => attempt,
853
+ Err(error) => {
854
+ error!(call_id = %delivery.call.call_id, %error, "failed to resolve delivery attempt");
855
+ finish_delivery(
856
+ &input_queue,
857
+ &delivery.receipt,
858
+ false,
859
+ &delivery.call.call_id,
860
+ lease,
861
+ &live_calls,
862
+ )
863
+ .await;
864
+ return;
865
+ }
866
+ };
780
867
  let DequeuedCall {
781
868
  call: work,
782
869
  receipt,
783
870
  } = delivery;
784
871
  let call_id = work.call_id.clone();
872
+ let final_attempt = match attempt {
873
+ DeliveryAttempt::Ready {
874
+ attempt,
875
+ max_attempts,
876
+ } => {
877
+ info!(%call_id, attempt, max_attempts, "executing coalesced queued call attempt");
878
+ attempt == max_attempts
879
+ }
880
+ DeliveryAttempt::Exhausted {
881
+ attempts,
882
+ max_attempts,
883
+ } => {
884
+ let message = format!(
885
+ "retry attempts exhausted after {attempts} deliveries ({max_attempts} executions allowed)"
886
+ );
887
+ let completed = write_terminal_error(
888
+ &queue,
889
+ &mut conn,
890
+ &work,
891
+ "RetryAttemptsExhausted",
892
+ &message,
893
+ None,
894
+ )
895
+ .await
896
+ .is_ok();
897
+ finish_delivery(
898
+ &input_queue,
899
+ &receipt,
900
+ completed,
901
+ &call_id,
902
+ lease,
903
+ &live_calls,
904
+ )
905
+ .await;
906
+ return;
907
+ }
908
+ DeliveryAttempt::OwnershipLost => {
909
+ finish_delivery(&input_queue, &receipt, false, &call_id, lease, &live_calls).await;
910
+ return;
911
+ }
912
+ };
785
913
  let completed = match coalesced_execute_and_write(
786
914
  &coalescing_queue,
787
915
  &queue,
788
916
  &mut conn,
789
917
  work,
918
+ final_attempt,
790
919
  metrics_pipeline.as_ref(),
791
920
  )
792
921
  .await
793
922
  {
794
- Ok(()) => true,
923
+ Ok(completed) => completed,
795
924
  Err(e) => {
796
925
  // The call's result stream may be incomplete; the enqueue safety TTL
797
926
  // and the meta status are the backstop for a stuck poller.
@@ -821,8 +950,9 @@ async fn coalesced_execute_and_write(
821
950
  queue: &FunctionQueue,
822
951
  conn: &mut ConnectionManager,
823
952
  work: QueuedCall,
953
+ final_attempt: bool,
824
954
  metrics_pipeline: Option<&Arc<PublishingMetricsPipeline>>,
825
- ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
955
+ ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
826
956
  let call_uuid = Uuid::parse_str(&work.call_id)?;
827
957
  let result_destination = ResultDestination::for_call(queue, &work, call_uuid);
828
958
  let ipc_bytes = work.call.feather_bytes()?;
@@ -881,6 +1011,10 @@ async fn coalesced_execute_and_write(
881
1011
  info!(call_id = %work.call_id, "coalesced queued call completed");
882
1012
  }
883
1013
  Some(Err(status)) => {
1014
+ if !final_attempt {
1015
+ warn!(call_id = %work.call_id, error = %status, "coalesced queued call failed; leaving delivery pending for retry");
1016
+ return Ok(false);
1017
+ }
884
1018
  // Parity with the enqueue/poll path: only a message string is
885
1019
  // available, so exc_type is a fixed placeholder.
886
1020
  result_destination
@@ -897,6 +1031,10 @@ async fn coalesced_execute_and_write(
897
1031
  // happen (it always sends one Ok/Err per caller), but treat a
898
1032
  // missing response as a failure rather than leaving the poller hung.
899
1033
  let msg = "coalesced dispatch produced no response";
1034
+ if !final_attempt {
1035
+ warn!(call_id = %work.call_id, "{msg}; leaving delivery pending for retry");
1036
+ return Ok(false);
1037
+ }
900
1038
  result_destination
901
1039
  .write_error(conn, 1, "RemoteFunctionError", msg, None)
902
1040
  .await?;
@@ -906,7 +1044,7 @@ async fn coalesced_execute_and_write(
906
1044
  error!(call_id = %work.call_id, "{msg}");
907
1045
  }
908
1046
  }
909
- Ok(())
1047
+ Ok(true)
910
1048
  }
911
1049
 
912
1050
  // Insert trace ID into headers if none already present
@@ -934,8 +1072,9 @@ async fn execute_and_write(
934
1072
  conn: &mut ConnectionManager,
935
1073
  work: QueuedCall,
936
1074
  flush_timeout: Duration,
1075
+ final_attempt: bool,
937
1076
  metrics_pipeline: Option<&Arc<PublishingMetricsPipeline>>,
938
- ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1077
+ ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
939
1078
  let call_uuid = Uuid::parse_str(&work.call_id)?;
940
1079
  let result_destination = ResultDestination::for_call(queue, &work, call_uuid);
941
1080
  let ipc_bytes = work.call.feather_bytes()?;
@@ -947,13 +1086,9 @@ async fn execute_and_write(
947
1086
  let _ = queue.update_status(call_uuid, "running").await;
948
1087
  info!(call_id = %work.call_id, function = %function_name, "running queued call");
949
1088
 
950
- // The handler appends each emitted chunk into `buffer` in real time — the
951
- // Python bridge calls the emitter per generator-yield. Stream those chunks
952
- // to the result stream *as they appear* rather than holding them until the
953
- // handler returns: we wait on the notify (signaled on each append) and flush
954
- // immediately when chunks arrive, so a slow generator's early yields reach
955
- // the client without waiting for the whole call to finish. A timeout fallback
956
- // ensures we make progress if the notification pipeline breaks.
1089
+ // The Python bridge appends each generator yield into `buffer`. Calls
1090
+ // without retries flush incrementally; retry-enabled calls retain chunks
1091
+ // until the attempt succeeds so a later attempt cannot duplicate output.
957
1092
  let buffer = ChunkBuffer::new().into_shared();
958
1093
  let handler_fut = handler.call_into_buffer(
959
1094
  ipc_bytes,
@@ -968,6 +1103,9 @@ async fn execute_and_write(
968
1103
  // seq 0. `flushed` is the count of chunks already written to the stream.
969
1104
  let mut seq: u64 = 1;
970
1105
  let mut flushed: usize = 0;
1106
+ // A retry must not duplicate generator chunks. Retry-enabled calls buffer
1107
+ // output until the handler succeeds or reaches its final attempt.
1108
+ let buffer_until_terminal = work.max_retries.is_some();
971
1109
 
972
1110
  // Completion is driven by the handler future resolving, NOT by the buffer
973
1111
  // emitter being torn down — so a handler that pins its emitter (e.g. via a
@@ -984,10 +1122,12 @@ async fn execute_and_write(
984
1122
  biased;
985
1123
  res = &mut handler_fut => break res,
986
1124
  _ = tokio::time::timeout(flush_timeout, notify_handle.notified()) => {
987
- if let Err(e) = flush_new(conn, &result_destination, &buffer, &mut flushed, &mut seq).await {
988
- // Non-fatal: keep draining; the cursor didn't advance past the
989
- // failed chunk, so the next wait retries it without duplicating.
990
- warn!(call_id = %work.call_id, error = %e, "failed to flush partial results; retrying");
1125
+ if !buffer_until_terminal {
1126
+ if let Err(e) = flush_new(conn, &result_destination, &buffer, &mut flushed, &mut seq).await {
1127
+ // Non-fatal: keep draining; the cursor didn't advance past the
1128
+ // failed chunk, so the next wait retries it without duplicating.
1129
+ warn!(call_id = %work.call_id, error = %e, "failed to flush partial results; retrying");
1130
+ }
991
1131
  }
992
1132
  }
993
1133
  }
@@ -1006,6 +1146,11 @@ async fn execute_and_write(
1006
1146
  );
1007
1147
  }
1008
1148
 
1149
+ if result.is_err() && !final_attempt {
1150
+ warn!(call_id = %work.call_id, "queued call failed; leaving delivery pending for retry");
1151
+ return Ok(false);
1152
+ }
1153
+
1009
1154
  // Flush whatever the handler produced after the loop exits (on the fast path
1010
1155
  // this is everything). This happens even if the handler panicked mid-execution,
1011
1156
  // so partial output is always preserved. Parity with the enqueue/poll path: a
@@ -1049,6 +1194,34 @@ async fn execute_and_write(
1049
1194
  );
1050
1195
  }
1051
1196
  }
1197
+ Ok(true)
1198
+ }
1199
+
1200
+ async fn write_terminal_error(
1201
+ queue: &FunctionQueue,
1202
+ conn: &mut ConnectionManager,
1203
+ work: &QueuedCall,
1204
+ exc_type: &str,
1205
+ message: &str,
1206
+ traceback: Option<&str>,
1207
+ ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1208
+ let call_uuid = Uuid::parse_str(&work.call_id)?;
1209
+ let result_destination = ResultDestination::for_call(queue, work, call_uuid);
1210
+ result_destination
1211
+ .write_error(conn, 1, exc_type, message, traceback)
1212
+ .await?;
1213
+ let summary: String = message.chars().take(200).collect();
1214
+ let processing_trace_id = work
1215
+ .metadata
1216
+ .otel_headers
1217
+ .get("traceparent")
1218
+ .and_then(|value| value.split('-').nth(1))
1219
+ .filter(|value| value.len() == 32)
1220
+ .map(str::to_string);
1221
+ let _ = queue
1222
+ .complete_status(call_uuid, "failed", &summary, processing_trace_id)
1223
+ .await;
1224
+ error!(call_id = %work.call_id, %message, "queued call reached terminal failure");
1052
1225
  Ok(())
1053
1226
  }
1054
1227
 
@@ -0,0 +1 @@
1
+ __version__ = "1.10.2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chalk-remote-call-python
3
- Version: 1.10.1
3
+ Version: 1.10.2
4
4
  Summary: Chalk remote call Python runtime interface client
5
5
  Author: Chalk AI, Inc.
6
6
  Project-URL: Homepage, https://chalk.ai
@@ -1 +0,0 @@
1
- __version__ = "1.10.1"