flow_chat 0.10.0 → 0.10.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a748d7d7a637ca0dc2e0f7c5d8dde213df8fd46eec9117417f141ed6d8aff91d
4
- data.tar.gz: ae52d99625691a839cda0e6884ab065bfc5851aa427ceafc4c19ded649c13da7
3
+ metadata.gz: 6ae06aa071a9ac6f07a9b2555e4e35dcaf98512962625b45426865484d989f7c
4
+ data.tar.gz: 583ef52a86cd6462dd3d0bb5e6b1cf76267ea3bbfcd0d9b33b2d7ceffa065e9e
5
5
  SHA512:
6
- metadata.gz: 05e3a2dc63c3c678f0da32f180121caece864af244181f1deb1b668118244e4c342e5656aac7533b79eb3d6f0b3f9f87c62c0ca9faa14243cf9c1ab04c06bea6
7
- data.tar.gz: 804ca337d0842bd077f2f2fbde1c864d993ba22d87915e92d869103b8d9adb8ff898cd598dc476bff7625772151be8f49e3853ec93d010cb646e19d9b54e8a06
6
+ metadata.gz: 22a819e0db8e6784933ff311b48aa3d9f62eb38aa456df985ee2b4d5c7d8fb683bc866efebe3643294b3ef49c1f5b041b1c6bd77a514e2f9dd63d913cc4e8a3a
7
+ data.tar.gz: bc2057d4d6aed38010c635fa56d91f756d5ecbbfc527d5b21ff2c4d04457b5672367321d00499d8e7a37c374352891ce15c14210c94738ef52114ff2676fdb1d
@@ -28,6 +28,35 @@ jobs:
28
28
  - name: Build gem
29
29
  run: bundle exec gem build *.gemspec
30
30
 
31
+ # Watches unreleased Rails, because flow_chat's activesupport and actionpack
32
+ # dependencies are deliberately unbounded above (see flow_chat.gemspec).
33
+ #
34
+ # Allowed to fail. A red run here means Rails main changed something this gem
35
+ # relies on, and is a prompt to look rather than a broken build - the point is
36
+ # to find out while the change is still unreleased, instead of from a bug
37
+ # report the week the major ships.
38
+ rails-main:
39
+ runs-on: ubuntu-latest
40
+ continue-on-error: true
41
+ env:
42
+ BUNDLE_GEMFILE: gemfiles/rails_main.gemfile
43
+
44
+ steps:
45
+ - uses: actions/checkout@v4
46
+
47
+ # Newest stable, deliberately ahead of the matrix above. Rails main already
48
+ # requires Ruby >= 3.3 (via ractor-dispatch) and that floor keeps rising, so
49
+ # pinning this job to the minimum would fail it for a Ruby version rather
50
+ # than for the API change it exists to catch.
51
+ - name: Set up Ruby
52
+ uses: ruby/setup-ruby@v1
53
+ with:
54
+ ruby-version: '3.4'
55
+ bundler-cache: true
56
+
57
+ - name: Run tests against rails main
58
+ run: bundle exec rake test
59
+
31
60
  lint:
32
61
  runs-on: ubuntu-latest
33
62
  steps:
data/.gitignore CHANGED
@@ -12,4 +12,7 @@
12
12
 
13
13
  # Ignore Gemfile.lock for our gem
14
14
  /Gemfile.lock
15
- .DS_Store
15
+ .DS_Store
16
+ # Resolved fresh on every CI run, so the rails-main job sees the latest main
17
+ # rather than a pinned revision.
18
+ gemfiles/*.lock
data/CHANGELOG.md CHANGED
@@ -2,8 +2,30 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.10.1] - 2026-08-17
6
+
7
+ ### Bug Fixes
8
+
9
+ - Publish side events when no foreground pass ran
10
+
11
+ ### Documentation
12
+
13
+ - Record #9 in the 0.10.0 release notes
14
+
15
+ ### Miscellaneous Tasks
16
+
17
+ - State the Ruby floor, and watch unreleased Rails instead of capping ([#10](https://github.com/radioactive-labs/flow_chat/issues/10))
18
+ - Prepend the changelog instead of regenerating it
19
+
5
20
  ## [0.10.0] - 2026-08-17
6
21
 
22
+ > **Note.** [#9](https://github.com/radioactive-labs/flow_chat/pull/9) was squash-merged with a non-conventional title, so git-cliff did not pick it up when this section was generated. It is the largest change in the release and is listed by hand below. Releases now prepend rather than regenerate, so this survives.
23
+
24
+ ### Behaviour changes
25
+
26
+ - **Intercom resolves numbers only.** The renderer already prints a number beside every option and asks for one; matching typed labels as well is what allowed two options reading the same to collapse onto a single mapping entry. Typing the words now falls through to the flow's own validation.
27
+ - **HTTP matches replies exactly**, with no case or whitespace folding. A web client echoes back the string it was handed, so nothing drifts in transit. A client that lowercases its reply will stop resolving.
28
+
7
29
  ### Bug Fixes
8
30
 
9
31
  - Correct runtime-breaking session and API misuse
@@ -18,6 +40,7 @@ All notable changes to this project will be documented in this file.
18
40
 
19
41
  ### Features
20
42
 
43
+ - Add Facebook Messenger and Instagram DM gateways, unify choice resolution, and fix delivery instrumentation ([#9](https://github.com/radioactive-labs/flow_chat/pull/9))
21
44
  - Map choices on Intercom and HTTP ([#4](https://github.com/radioactive-labs/flow_chat/issues/4))
22
45
  - Let a flow declare that it consumed the turn ([#6](https://github.com/radioactive-labs/flow_chat/issues/6))
23
46
  - Report a reply the platform would not take ([#7](https://github.com/radioactive-labs/flow_chat/issues/7))
data/flow_chat.gemspec CHANGED
@@ -14,7 +14,11 @@ Gem::Specification.new do |spec|
14
14
  DESC
15
15
  spec.homepage = "https://github.com/radioactive-labs/flow_chat"
16
16
  spec.license = "MIT"
17
- spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
17
+ # Matches the versions CI actually exercises. The previous ">= 2.3.0" was
18
+ # three Rubies out of date and promised a floor nothing here could honour:
19
+ # activesupport 8 requires Ruby 3.2, so a 2.x user hit an unexplained
20
+ # resolution failure rather than a clear statement of what this gem needs.
21
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0")
18
22
 
19
23
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
24
  spec.metadata["rubygems_mfa_required"] = "true"
@@ -32,9 +36,25 @@ Gem::Specification.new do |spec|
32
36
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
37
  spec.require_paths = ["lib"]
34
38
 
35
- spec.add_dependency "zeitwerk"
39
+ # Deliberately unbounded above, and RubyGems' warning about that is
40
+ # overridden knowingly rather than overlooked.
41
+ #
42
+ # A "< N" cap does not protect this gem from the next major - it decides
43
+ # where the breakage surfaces. Capping blocks every downstream application on
44
+ # the day that major ships, until this gem cuts a release, which is a cost
45
+ # paid by everyone for a break that may never come.
46
+ #
47
+ # Leaving it open moves the risk from install time to runtime, and the CI job
48
+ # against rails main is what pays for that: an incompatible change is found
49
+ # while it is still unreleased, rather than in a bug report the week it lands.
50
+ # If that job is ever removed, these bounds should be revisited.
51
+ #
52
+ # No floor is needed beyond ">= 6" either. Bundler resolves an activesupport
53
+ # that suits the running Ruby, so a Ruby 3.0 application lands on Rails 7.x
54
+ # without this gem having to say so.
36
55
  spec.add_dependency "activesupport", ">= 6"
37
56
  spec.add_dependency "actionpack", ">= 6"
57
+ spec.add_dependency "zeitwerk"
38
58
  spec.add_dependency "phonelib"
39
59
  spec.add_dependency "ibsciss-middleware", "~> 0.4.2"
40
60
  spec.add_dependency "intercom", "~> 4.2"
@@ -0,0 +1,18 @@
1
+ # Resolves against unreleased Rails, for the CI job that watches for breaking
2
+ # changes before they ship.
3
+ #
4
+ # This exists because flow_chat's activesupport and actionpack dependencies are
5
+ # deliberately unbounded above (see flow_chat.gemspec). Without an upper bound,
6
+ # an incompatible major would otherwise be discovered by an application that
7
+ # upgraded, rather than here. The job is allowed to fail: a red run is a signal
8
+ # to look, not a broken build.
9
+ #
10
+ # The root Gemfile is evaluated rather than restated, so this file carries the
11
+ # development dependencies too - rake, minitest, standard and the rest. Listing
12
+ # only the gemspec here left the suite unable to load its own Rakefile, which
13
+ # failed the job for a reason that had nothing to do with Rails.
14
+ eval_gemfile File.expand_path("../Gemfile", __dir__)
15
+
16
+ # Overrides the activesupport/actionpack the gemspec resolved, which is the
17
+ # whole point of this file.
18
+ gem "rails", github: "rails/rails", branch: "main"
@@ -70,9 +70,19 @@ module FlowChat
70
70
  def initialize(request_data)
71
71
  FlowChat.logger.debug { "BackgroundController: Initializing with request data" }
72
72
  @request = BackgroundRequest.new(request_data)
73
+ @side_events_published = request_data[:side_events_published] || false
73
74
  @response = nil
74
75
  end
75
76
 
77
+ # Whether the pass that enqueued this job already announced the delivery's
78
+ # side events. Only the gem sets it, when it enqueues a job of its own.
79
+ # An application that builds a request context by hand and enqueues
80
+ # directly has no earlier pass behind it, so the answer is false and the
81
+ # job is the one that announces them.
82
+ def side_events_published?
83
+ @side_events_published
84
+ end
85
+
76
86
  # Delegate params to request (mimics Rails controller behavior)
77
87
  def params
78
88
  request.params
@@ -17,6 +17,18 @@ module FlowChat
17
17
  @controller.is_a?(::FlowChat::BackgroundController)
18
18
  end
19
19
 
20
+ # Whether an earlier pass already announced this delivery's side events,
21
+ # which is the case only for a job the gem enqueued after publishing them
22
+ # itself. Being in the background is not enough on its own: an application
23
+ # may build a request context and enqueue a job with no foreground pass
24
+ # ahead of it, and then this pass is the first to see the delivery.
25
+ #
26
+ # in_background? short-circuits because a foreground controller is a Rails
27
+ # controller, which knows nothing about side events.
28
+ def side_events_already_published?
29
+ in_background? && @controller.side_events_published?
30
+ end
31
+
20
32
  # Check if async processing should be used
21
33
  # Returns true if:
22
34
  # - Not already in background mode
@@ -40,6 +52,11 @@ module FlowChat
40
52
  FlowChat.logger.info { "#{self.class.name}: Async enabled - enqueuing background job" }
41
53
 
42
54
  # Serialize request data for BackgroundController
55
+ #
56
+ # side_events_published records that this pass has already announced the
57
+ # delivery's side events, so the job it is about to enqueue does not
58
+ # announce them a second time. It rides in the request context rather
59
+ # than in params, where a platform's own webhook fields live.
43
60
  request_data = {
44
61
  params: @controller.request.params.to_h,
45
62
  method: @controller.request.method,
@@ -47,7 +64,8 @@ module FlowChat
47
64
  host: extract_host(@controller.request),
48
65
  path: extract_path(@controller.request),
49
66
  body: extract_body_for_background(@controller.request),
50
- remote_ip: extract_remote_ip(@controller.request)
67
+ remote_ip: extract_remote_ip(@controller.request),
68
+ side_events_published: true
51
69
  }
52
70
 
53
71
  # Enqueue user's job with request context and job params
@@ -111,20 +111,23 @@ module FlowChat
111
111
  flow_ran = false
112
112
 
113
113
  entries.each do |entry|
114
- # Published in the foreground only, and always - whether this turn
115
- # goes on to run a flow inline or hand one to a background job.
114
+ # Published by whichever pass is first to see this delivery, and
115
+ # only once.
116
116
  #
117
- # With async enabled the job re-enters this method on the same body,
118
- # so publishing here as well as there announced every receipt, echo
119
- # and standby event twice: once by the request, once by the job.
120
- # Skipping them in the background leaves exactly one publisher, and
121
- # leaves it the one that is already holding the delivery - so a
122
- # receipt is announced when it arrives rather than whenever the queue
123
- # gets to it, and survives a job that is never picked up.
117
+ # With async enabled the gem publishes here, enqueues, and the job
118
+ # re-enters this method on the same body, so publishing in both
119
+ # announced every receipt, echo and standby event twice. The job the
120
+ # gem enqueues therefore carries word that they are already out, and
121
+ # skips them. The request is the better publisher of the two: it
122
+ # already holds the delivery, so a receipt is announced when it
123
+ # arrives rather than whenever the queue gets to it, and survives a
124
+ # job that is never picked up.
124
125
  #
125
- # Nothing changes for an app that does not use async: it never runs
126
- # in the background, so it takes this branch every time.
127
- publish_side_events(entry) unless in_background?
126
+ # A background pass with nothing behind it publishes. An application
127
+ # is free to build its own request context and enqueue a job itself,
128
+ # which is what fanning one shared webhook out to several accounts
129
+ # takes, and no foreground pass ever runs for those.
130
+ publish_side_events(entry) unless side_events_already_published?
128
131
 
129
132
  events = entry["messaging"]
130
133
  next unless events.is_a?(Array)
@@ -1,3 +1,3 @@
1
1
  module FlowChat
2
- VERSION = "0.10.0"
2
+ VERSION = "0.10.1"
3
3
  end
@@ -147,13 +147,18 @@ module FlowChat
147
147
  # slot is claimed, or a status arriving ahead of a message in the
148
148
  # same delivery would spend the slot and drop the message.
149
149
  #
150
- # Foreground only. With async enabled the job re-enters this
151
- # method on the same body, so publishing in both announced every
152
- # status twice and delivery receipts double-counted. The request
153
- # publishes them: it already holds the delivery, so a receipt is
154
- # announced when it arrives rather than when the queue reaches
155
- # it, and survives a job that is never picked up.
156
- handle_statuses(value) if value["statuses"].present? && !in_background?
150
+ # Announced by whichever pass is first to see the delivery, and
151
+ # only once. With async enabled the gem publishes here, enqueues,
152
+ # and the job re-enters this method on the same body, so
153
+ # publishing in both double-counted every receipt. The job the
154
+ # gem enqueues carries word that they are already out and skips
155
+ # them, which leaves the request publishing: it already holds
156
+ # the delivery, so a receipt is announced when it arrives rather
157
+ # than when the queue reaches it, and survives a job that is
158
+ # never picked up. A background pass with no foreground pass
159
+ # behind it, as an application enqueuing its own jobs has,
160
+ # publishes them itself.
161
+ handle_statuses(value) if value["statuses"].present? && !side_events_already_published?
157
162
 
158
163
  next if value["messages"].blank?
159
164
 
@@ -171,13 +176,13 @@ module FlowChat
171
176
  when "statuses"
172
177
  # Only reachable for a payload built without a field name, which
173
178
  # our own fixtures do and Meta does not.
174
- handle_statuses(value) unless in_background?
179
+ handle_statuses(value) unless side_events_already_published?
175
180
  else
176
181
  # Anything that is not a message or its delivery. Coexistence
177
182
  # echoes, contact syncs, imported history, account bans, template
178
183
  # approvals: all of it is the application's domain, so it is
179
184
  # published rather than interpreted here.
180
- handle_unmodelled_field(change["field"], value, entry["id"]) unless in_background?
185
+ handle_unmodelled_field(change["field"], value, entry["id"]) unless side_events_already_published?
181
186
  end
182
187
  end
183
188
  end
@@ -94,8 +94,18 @@ namespace :release do
94
94
  end
95
95
 
96
96
  # Changelog — same config CI uses for release notes, so they agree.
97
+ #
98
+ # Prepends the new section rather than regenerating the file. `-o` rewrote
99
+ # CHANGELOG.md from the commit history every time, so anything hand-written
100
+ # was silently discarded at the next release. That is not hypothetical: #9
101
+ # was squash-merged with a non-conventional title, git-cliff could not
102
+ # categorise it, and the largest change in v0.10.0 had to be written in by
103
+ # hand — which `-o` would have erased.
104
+ #
105
+ # --unreleased limits the run to commits since the last tag, so only the
106
+ # section being cut is generated and everything below it is left alone.
97
107
  abort "git-cliff not found. Install with: brew install git-cliff" unless git_cliff?
98
- system("git-cliff", "--config", RELEASE_CLIFF_CONFIG, "--tag", "v#{version}", "-o", "CHANGELOG.md") ||
108
+ system("git-cliff", "--config", RELEASE_CLIFF_CONFIG, "--tag", "v#{version}", "--unreleased", "--prepend", "CHANGELOG.md") ||
99
109
  abort("Changelog generation failed")
100
110
  puts "✓ CHANGELOG.md"
101
111
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flow_chat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
@@ -11,21 +11,21 @@ cert_chain: []
11
11
  date: 2026-08-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: zeitwerk
14
+ name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '6'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '6'
27
27
  - !ruby/object:Gem::Dependency
28
- name: activesupport
28
+ name: actionpack
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -39,19 +39,19 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '6'
41
41
  - !ruby/object:Gem::Dependency
42
- name: actionpack
42
+ name: zeitwerk
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '6'
47
+ version: '0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '6'
54
+ version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: phonelib
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -183,6 +183,7 @@ files:
183
183
  - examples/whatsapp_controller.rb
184
184
  - examples/whatsapp_media_examples.rb
185
185
  - flow_chat.gemspec
186
+ - gemfiles/rails_main.gemfile
186
187
  - lib/flow_chat.rb
187
188
  - lib/flow_chat/app.rb
188
189
  - lib/flow_chat/async_job.rb
@@ -285,7 +286,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
285
286
  requirements:
286
287
  - - ">="
287
288
  - !ruby/object:Gem::Version
288
- version: 2.3.0
289
+ version: '3.0'
289
290
  required_rubygems_version: !ruby/object:Gem::Requirement
290
291
  requirements:
291
292
  - - ">="