aidaemon 0.11.10

A personal AI agent that runs as a background daemon, accessible via Telegram, Slack, or Discord, with tool use, MCP integration, and persistent memory
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
// ==================== Orchestration Integration Tests ====================

#[tokio::test]
async fn test_orchestration_uniform_models_no_routing() {
    // With uniform models (no router), first-pass orchestration does not
    // activate, so simple messages get direct responses.
    let provider = MockProvider::new(); // Returns "Mock response"
    let harness = setup_test_agent(provider).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "test_session",
            "Hello!",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert_eq!(response, "Mock response");

    // No goals — uniform models bypass orchestration routing
    let goals = harness.state.get_active_goals().await.unwrap();
    assert!(goals.is_empty(), "No goals with uniform models");
}

#[tokio::test]
async fn test_orchestration_simple_falls_through_to_full_loop() {
    // Deterministic routing classifies this as a simple task -> full agent loop.
    let provider = MockProvider::with_responses(vec![
        // 1st call: deferral text, bounced by the deferred-action gate
        MockProvider::text_response("I'll check the system info and get back to you."),
        // 2nd call: full agent loop — tool call
        MockProvider::tool_call_response("system_info", "{}"),
        // 3rd call: full agent loop — final response
        MockProvider::text_response("Your system is running macOS."),
    ]);
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "test_session",
            "check system info",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    // Should get the full agent loop's response
    assert_eq!(response, "Your system is running macOS.");

    // No goals should be created (simple tasks don't create goals)
    let goals = harness.state.get_active_goals().await.unwrap();
    assert!(goals.is_empty(), "Simple tasks should not create goals");
}

#[tokio::test]
async fn test_orchestration_complex_creates_goal() {
    // Deterministic pre-routing classifies the request as complex based on
    // keyword heuristics (action markers + compound keywords), creates a goal,
    // and spawns a task lead synchronously (no self_ref in tests).
    // No orchestration LLM call — routing is purely deterministic.
    let provider = MockProvider::with_responses(vec![
        // Task lead's LLM call — deterministic routing creates the goal
        // without an LLM call, then the task lead runs the agent loop.
        MockProvider::text_response("I'll start working on building your website."),
    ]);
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    // User text must trigger looks_like_complex_request_fallback():
    // >=3 action markers (analyze, compare, identify, find, summarize) + compound ("and"/"then")
    let response = harness
        .agent
        .handle_message(
            "test_session",
            "Analyze the requirements and compare authentication libraries, then identify the best frameworks, find suitable database solutions, and summarize the deployment options for a full-stack website with CI/CD pipeline",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    // Should get a response (the exact text depends on how many LLM calls the agent loop makes)
    assert!(!response.is_empty(), "Should return a non-empty response");

    // The key assertion: a goal should have been created
    let goals = harness
        .state
        .get_goals_for_session("test_session")
        .await
        .unwrap();
    assert_eq!(goals.len(), 1, "Complex request should create a goal");
    // Text-only task-lead replies must not auto-complete the goal without finished tasks.
    assert_eq!(goals[0].status, "active");
    assert!(goals[0]
        .description
        .contains("Analyze the requirements"));
}

#[tokio::test]
async fn test_orchestration_complex_internal_maintenance_does_not_create_goal() {
    // Deterministic routing classifies this as complex (action markers trigger
    // looks_like_complex_request_fallback), but handle_complex_intent detects
    // maintenance keywords and returns a canned response without creating a goal.
    // No LLM calls needed — both routing and maintenance detection are deterministic.
    let provider = MockProvider::new(); // No scripted responses needed
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    // User text triggers complex classification (analyze, identify, find, report = 4 action
    // markers + "and"/"then") AND matches is_internal_maintenance_intent (process embeddings
    // + consolidate memories + decay old facts).
    let response = harness
        .agent
        .handle_message(
            "test_session",
            "Analyze the knowledge base and identify stale entries, then process embeddings, consolidate memories, find outdated data, and report on decay old facts",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert!(
        response.contains("already runs via built-in background jobs"),
        "Expected maintenance-routing response, got: {response}"
    );

    let goals = harness
        .state
        .get_goals_for_session("test_session")
        .await
        .unwrap();
    assert!(
        goals.is_empty(),
        "Internal maintenance intent should not create a goal"
    );
}

#[tokio::test]
async fn test_orchestration_simple_stall_detection_in_full_loop() {
    // Simple tasks now go through full agent loop which has its own stall detection.
    // After the first routing pass, repeated identical tool calls should be detected.
    let provider = MockProvider::with_responses(vec![
        // 1st call: deferral text, bounced by the deferred-action gate
        MockProvider::text_response("I'll run a command for you."),
        // 2nd call: real tool call
        MockProvider::tool_call_response("system_info", "{}"),
        // Repeated identical tool calls — stall detection should kick in
        MockProvider::tool_call_response("system_info", "{}"),
        MockProvider::tool_call_response("system_info", "{}"),
        MockProvider::tool_call_response("system_info", "{}"),
        MockProvider::tool_call_response("system_info", "{}"),
        MockProvider::tool_call_response("system_info", "{}"),
        // Enough repetitions to trigger stall detection
        MockProvider::text_response("Should not reach here"),
    ]);
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "test_session",
            "run a quick check",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    // Full loop stall detection produces graceful responses
    assert!(
        !response.is_empty(),
        "Should return a non-empty response even on stall"
    );
}

#[tokio::test]
async fn test_orchestration_simple_uses_full_loop_with_all_tools() {
    // Simple tasks now use the full agent loop with all tools available.
    // Verify the agent can complete a simple task through the full loop.
    let provider = MockProvider::with_responses(vec![
        // 1st call: deferral text, bounced by the deferred-action gate
        MockProvider::text_response("I'll run the diagnostics and get back to you."),
        // 2nd call: deferred-action retry produces a real tool call
        MockProvider::tool_call_response("system_info", "{}"),
        // 3rd-5th calls: final response, repeated to survive mutation-contract
        // nudges ("run" triggers expects_mutation=true → up to 2 extra iterations
        // before text response is accepted).
        MockProvider::text_response("Diagnostics complete. All systems normal."),
        MockProvider::text_response("Diagnostics complete. All systems normal."),
        MockProvider::text_response("Diagnostics complete. All systems normal."),
    ]);
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "test_session",
            "run diagnostics",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert_eq!(response, "Diagnostics complete. All systems normal.");
}

#[tokio::test]
async fn test_personal_recall_challenge_scopes_tools_and_reaffirms() {
    let provider = MockProvider::with_responses(vec![
        // Recall turns accept substantive text replies readily, so the first
        // response is the out-of-scope tool call this test is about.
        {
            let mut resp = MockProvider::tool_call_response(
                "browser",
                r#"{"action":"navigate","url":"https://example.com"}"#,
            );
            resp.content = Some("I'll check additional sources.".to_string());
            resp
        },
        {
            let mut resp = MockProvider::tool_call_response(
                "manage_people",
                r#"{"action":"view","person_name":"__unknown_person_for_recall_guardrail__"}"#,
            );
            resp.content = Some("I'll re-check your stored people data.".to_string());
            resp
        },
        MockProvider::text_response("I still do not have that information saved in memory."),
    ]);
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "test_session",
            "Are you sure I have pets?",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert!(
        response.contains("do not have"),
        "Expected no-information reaffirmation after targeted memory re-check, got: {}",
        response
    );
    assert!(
        harness.provider.call_count().await <= 4,
        "Challenge turn should stay bounded and not spiral"
    );

    let history = harness.state.get_history("test_session", 50).await.unwrap();
    let browser_tool_msgs: Vec<&crate::traits::Message> = history
        .iter()
        .filter(|m| m.role == "tool" && m.tool_name.as_deref() == Some("browser"))
        .collect();
    let scoped_block = !browser_tool_msgs.is_empty()
        && browser_tool_msgs.iter().all(|m| {
            m.content.as_deref().is_some_and(|c| {
                c.contains("Personal-memory recall")
                    || c.contains("not a real tool")
                    || c.contains("Unknown tool")
                    || c.contains("should be answered directly in plain text")
            })
        });
    // The browser tool call may be blocked by the personal-recall scope
    // guard OR by the text-only prelude check (for non-mutation turns).
    // Either path prevents execution.
    assert!(
        scoped_block,
        "Expected out-of-scope browser tool call to be blocked for personal recall turn"
    );
}

#[tokio::test]
async fn test_personal_recall_challenge_inherits_previous_turn_context() {
    // With deterministic routing (no orchestration LLM pass), each turn makes a
    // single LLM call through the normal agent loop. The deterministic routing
    // classifies both messages as Simple (no action markers, no schedule) and
    // falls through to the execution loop.
    let provider = MockProvider::with_responses(vec![
        // Turn 1: execution loop — direct answer
        MockProvider::text_response("I don't have information about pets."),
        // Turn 2: execution loop — reaffirmation
        MockProvider::text_response("I still do not have that information saved in memory."),
    ]);
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let first = harness
        .agent
        .handle_message(
            "test_session",
            "What about pets?",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();
    assert!(
        first.contains("don't have information about pets"),
        "Expected personal-recall context, got: {}",
        first
    );

    let second = harness
        .agent
        .handle_message(
            "test_session",
            "Are you sure?",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();
    assert!(
        second.contains("do not have") || second.contains("requires running tools"),
        "Expected no-information reaffirmation or tools-unavailable message, got: {}",
        second
    );
    // No text-only pre-pass: 1 LLM call per turn x 2 turns = 2
    assert!(
        harness.provider.call_count().await <= 3,
        "Follow-up challenge should stay bounded and not spiral"
    );
}

#[tokio::test]
async fn test_general_reaffirmation_challenge_injects_prior_answer_anchor() {
    let provider = MockProvider::with_responses(vec![
        MockProvider::text_response("There are 3 R's in strawberry."),
        MockProvider::text_response("Yes — strawberry has 3 R's."),
    ]);
    let harness = setup_test_agent(provider).await.unwrap();

    let first = harness
        .agent
        .handle_message(
            "reaffirm_anchor_test",
            "How many R's in strawberry?",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();
    assert!(
        first.contains("3 R"),
        "Expected strawberry answer, got: {}",
        first
    );

    let _second = harness
        .agent
        .handle_message(
            "reaffirm_anchor_test",
            "Are you sure?",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    let call_log = harness.provider.call_log.lock().await;
    let challenge_call = call_log.last().expect("challenge turn LLM call");
    let has_anchor = challenge_call.messages.iter().any(|message| {
        message
            .get("content")
            .and_then(|content| content.as_str())
            .is_some_and(|text| {
                text.contains("REAFFIRMATION CHALLENGE")
                    && text.contains("How many R's in strawberry?")
                    && text.contains("There are 3 R's in strawberry.")
            })
    });
    assert!(
        has_anchor,
        "Challenge turn should inject reaffirmation anchor for the immediately previous exchange: {:?}",
        challenge_call.messages
    );
}

#[tokio::test]
async fn test_compound_message_with_challenge_keyword_skips_reaffirmation_anchor() {
    let provider = MockProvider::with_responses(vec![
        MockProvider::text_response("There are 3 R's in strawberry."),
        MockProvider::text_response("Here is the blog post about Ecuador."),
    ]);
    let harness = setup_test_agent(provider).await.unwrap();

    harness
        .agent
        .handle_message(
            "reaffirm_anchor_negative_test",
            "How many R's in strawberry?",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    // Contains the word "really" but is a new task, not a vague challenge of
    // the previous answer — the anchor directive must NOT be injected.
    harness
        .agent
        .handle_message(
            "reaffirm_anchor_negative_test",
            "I really need you to write a blog post about Ecuador",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    let call_log = harness.provider.call_log.lock().await;
    let second_call = call_log.last().expect("second turn LLM call");
    let has_anchor = second_call.messages.iter().any(|message| {
        message
            .get("content")
            .and_then(|content| content.as_str())
            .is_some_and(|text| text.contains("REAFFIRMATION CHALLENGE"))
    });
    assert!(
        !has_anchor,
        "Compound new-task message must not be pinned to the previous exchange: {:?}",
        second_call.messages
    );
}

#[tokio::test]
async fn test_orchestration_scheduled_one_shot_creates_pending_confirmation() {
    // Deterministic schedule heuristics route before any LLM call.
    let provider = MockProvider::new();
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "test_session",
            "deploy in 2 hours",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert!(
        response.contains("Reply **confirm**"),
        "Expected confirmation prompt for scheduled goal"
    );

    let goals = harness
        .state
        .get_goals_for_session("test_session")
        .await
        .unwrap();
    assert_eq!(goals.len(), 1);
    assert_eq!(goals[0].goal_type, "finite");
    assert_eq!(goals[0].status, "pending_confirmation");
    let schedules = harness
        .state
        .get_schedules_for_goal(&goals[0].id)
        .await
        .unwrap();
    assert_eq!(schedules.len(), 1);
    assert!(schedules[0].is_one_shot);
}

#[tokio::test]
async fn test_orchestration_scheduled_malformed_schedule_recovers_from_user_text() {
    // The schedule phrase is extracted from the user text by the deterministic
    // heuristic; no LLM call is needed to take the scheduler path.
    let provider = MockProvider::new();
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "test_session",
            "check disk space in 2 minutes",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert!(
        response.contains("Reply **confirm**"),
        "Expected confirmation prompt for scheduled goal"
    );

    let goals = harness
        .state
        .get_goals_for_session("test_session")
        .await
        .unwrap();
    assert_eq!(goals.len(), 1);
    assert_eq!(goals[0].status, "pending_confirmation");
    let schedules = harness
        .state
        .get_schedules_for_goal(&goals[0].id)
        .await
        .unwrap();
    assert_eq!(schedules.len(), 1);
    assert!(schedules[0].is_one_shot);
}

#[tokio::test]
async fn test_orchestration_scheduled_recurring_creates_pending_confirmation() {
    // Deterministic schedule heuristics route before any LLM call.
    let provider = MockProvider::new();
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "test_session",
            "monitor API health every 6h",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert!(
        response.contains("Reply **confirm**"),
        "Expected confirmation prompt for recurring schedule"
    );

    let goals = harness
        .state
        .get_goals_for_session("test_session")
        .await
        .unwrap();
    assert_eq!(goals.len(), 1);
    assert_eq!(goals[0].goal_type, "continuous");
    assert_eq!(goals[0].status, "pending_confirmation");
    assert_eq!(goals[0].budget_per_check, Some(100_000));
    assert_eq!(goals[0].budget_daily, Some(500_000));
    let schedules = harness
        .state
        .get_schedules_for_goal(&goals[0].id)
        .await
        .unwrap();
    assert_eq!(schedules.len(), 1);
    assert!(!schedules[0].is_one_shot);
}

#[tokio::test]
async fn test_orchestration_scheduled_multi_segment_creates_two_pending_goals() {
    let provider = MockProvider::new(); // Deterministic schedule heuristics path
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "test_session",
            "1) every day at 9am remind me to check server health. 2) in 2 hours send status report",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert!(
        response.contains("Reply **confirm**"),
        "Expected confirmation prompt for multi-schedule request"
    );
    assert!(
        response.contains("2 goals"),
        "Expected batch confirmation text, got: {response}"
    );

    let goals = harness
        .state
        .get_goals_for_session("test_session")
        .await
        .unwrap();
    assert_eq!(goals.len(), 2);
    assert!(
        goals.iter().all(|goal| goal.status == "pending_confirmation"),
        "All goals should await confirmation"
    );
    assert!(
        goals.iter().any(|goal| goal.description == "Check server health"),
        "Expected cleaned recurring description"
    );
    assert!(
        goals.iter().any(|goal| goal.description == "Send status report"),
        "Expected cleaned one-shot description"
    );
}

#[tokio::test]
async fn test_orchestration_scheduled_multi_segment_confirm_and_cancel() {
    let provider = MockProvider::new(); // Deterministic schedule heuristics path
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let _ = harness
        .agent
        .handle_message(
            "test_session_confirm",
            "1) every day at 9am check server health. 2) in 2 hours send status report",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();
    let confirm_response = harness
        .agent
        .handle_message(
            "test_session_confirm",
            "confirm",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();
    assert!(
        confirm_response.contains("Scheduled 2 goals"),
        "Expected batch activation, got: {confirm_response}"
    );
    let confirm_goals = harness
        .state
        .get_goals_for_session("test_session_confirm")
        .await
        .unwrap();
    assert_eq!(confirm_goals.len(), 2);
    assert!(confirm_goals.iter().all(|goal| goal.status == "active"));

    let _ = harness
        .agent
        .handle_message(
            "test_session_cancel",
            "1) every day at 9am check server health. 2) in 2 hours send status report",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();
    let cancel_response = harness
        .agent
        .handle_message(
            "test_session_cancel",
            "cancel",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();
    assert!(
        cancel_response.contains("cancelled 2 goals"),
        "Expected batch cancellation, got: {cancel_response}"
    );
    let cancel_goals = harness
        .state
        .get_goals_for_session("test_session_cancel")
        .await
        .unwrap();
    assert_eq!(cancel_goals.len(), 2);
    assert!(
        cancel_goals
            .iter()
            .all(|goal| goal.status == "cancelled")
    );
}

#[tokio::test]
async fn test_orchestration_scheduled_multi_segment_auto_confirms_when_session_preapproved() {
    let provider = MockProvider::new(); // Deterministic schedule heuristics path
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();
    harness
        .agent
        .set_test_schedule_approval_for_session("test_session_preapproved_multi", true)
        .await;

    let response = harness
        .agent
        .handle_message(
            "test_session_preapproved_multi",
            "1) every day at 9am check server health. 2) in 2 hours send status report",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert!(
        response.contains("Scheduled 2 goals"),
        "Expected auto-confirmed batch activation, got: {response}"
    );

    let goals = harness
        .state
        .get_goals_for_session("test_session_preapproved_multi")
        .await
        .unwrap();
    assert_eq!(goals.len(), 2);
    assert!(goals.iter().all(|goal| goal.status == "active"));
    assert_eq!(
        harness.provider.call_count().await,
        0,
        "Auto-approved scheduling should not require LLM calls"
    );
}

#[tokio::test]
async fn test_orchestration_scheduled_multi_segment_rejects_too_many_segments() {
    let provider = MockProvider::new(); // Deterministic schedule heuristics path
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let request = (1..=11)
        .map(|n| format!("{n}) in {n}h run task {n}"))
        .collect::<Vec<_>>()
        .join(". ");

    let response = harness
        .agent
        .handle_message(
            "test_session_too_many_segments",
            &request,
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert!(
        response.contains("up to 10 goals per message"),
        "Expected segment-limit guidance, got: {response}"
    );

    let goals = harness
        .state
        .get_goals_for_session("test_session_too_many_segments")
        .await
        .unwrap();
    assert!(
        goals.is_empty(),
        "Segment-limit rejection should not create any goals"
    );
}

#[tokio::test]
async fn test_orchestration_scheduled_multi_segment_invalid_segment_rejected_without_partial_goal_creation(
) {
    let provider = MockProvider::new(); // Deterministic scheduling path should reject directly
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "test_session_invalid_multi",
            "1) every day at 9am check server health. 2) every 0m send status report",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert!(
        response.contains("couldn't parse one of the schedules"),
        "Expected direct invalid-segment response, got: {response}"
    );

    let goals = harness
        .state
        .get_goals_for_session("test_session_invalid_multi")
        .await
        .unwrap();
    assert!(
        goals.is_empty(),
        "Invalid multi-schedule input should not create partial goals"
    );
    assert!(
        harness.provider.call_count().await == 0,
        "Invalid multi-schedule input should not enter the LLM fallback loop"
    );
}

#[tokio::test]
async fn test_orchestration_scheduled_multi_segment_invalid_first_segment_creates_no_goals() {
    let provider = MockProvider::new(); // Deterministic scheduling path should reject directly
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "test_session_invalid_first_multi",
            "1) every 0m check server health. 2) in 2 hours send status report",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert!(
        response.contains("couldn't parse one of the schedules"),
        "Expected direct invalid-segment response, got: {response}"
    );
    let goals = harness
        .state
        .get_goals_for_session("test_session_invalid_first_multi")
        .await
        .unwrap();
    assert!(
        goals.is_empty(),
        "Invalid first segment should prevent all goal creation"
    );
    assert_eq!(
        harness.provider.call_count().await,
        0,
        "Invalid first segment should not trigger the LLM fallback loop"
    );
}

#[tokio::test]
async fn test_orchestration_scheduled_multi_segment_invalid_middle_segment_creates_no_goals() {
    let provider = MockProvider::new(); // Deterministic scheduling path should reject directly
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "test_session_invalid_middle_multi",
            "1) every day at 9am check server health. 2) every 0m send status report. 3) in 3 hours check alerts",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert!(
        response.contains("couldn't parse one of the schedules"),
        "Expected direct invalid-segment response, got: {response}"
    );
    let goals = harness
        .state
        .get_goals_for_session("test_session_invalid_middle_multi")
        .await
        .unwrap();
    assert!(
        goals.is_empty(),
        "Invalid middle segment should prevent all goal creation"
    );
    assert_eq!(
        harness.provider.call_count().await,
        0,
        "Invalid middle segment should not trigger the LLM fallback loop"
    );
}

#[tokio::test]
async fn test_orchestration_scheduled_single_description_uses_current_turn_task_text() {
    let provider = MockProvider::new();
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let _ = harness
        .agent
        .handle_message(
            "test_session_description_contamination",
            "What is my daily budget?",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    let _ = harness
        .agent
        .handle_message(
            "test_session_description_contamination",
            "tomorrow at 11:09pm EST remind me to check logs",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    let goals = harness
        .state
        .get_goals_for_session("test_session_description_contamination")
        .await
        .unwrap();
    assert_eq!(goals.len(), 1);
    // Reminder-shaped requests keep the canonical "Remind me to ..." form so
    // the fire-time reminder fast path can recognize them.
    assert_eq!(goals[0].description, "Remind me to check logs");
    assert!(
        !goals[0].description.contains("daily budget"),
        "Description should not include unrelated prior turn text"
    );
    // Plain one-shot reminders are auto-confirmed (no approval gate).
    assert_eq!(goals[0].status, "active");
}

#[tokio::test]
async fn test_orchestration_schedule_confirm_activates_goal() {
    // Deterministic routing detects "in 2 hours" via schedule heuristic (no LLM call).
    // "confirm" is handled by the confirmation gate (also no LLM call).
    // Total LLM calls across both messages = 0.
    let provider = MockProvider::new(); // No scripted responses needed
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let _ = harness
        .agent
        .handle_message(
            "test_session",
            "deploy in 2 hours",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    let confirm_response = harness
        .agent
        .handle_message(
            "test_session",
            "confirm",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();
    assert!(confirm_response.contains("Scheduled:"));

    let goals = harness
        .state
        .get_goals_for_session("test_session")
        .await
        .unwrap();
    assert_eq!(goals.len(), 1);
    assert_eq!(goals[0].status, "active");
    assert_eq!(
        harness.provider.call_count().await,
        0,
        "Both schedule creation (deterministic) and confirm (gate) should work without LLM calls"
    );
}

#[tokio::test]
async fn test_orchestration_schedule_cancel_removes_goal() {
    // Deterministic schedule heuristics route turn 1, and the deterministic
    // cancel shortcut handles turn 2 — no LLM calls needed.
    let provider = MockProvider::new();
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let _ = harness
        .agent
        .handle_message(
            "test_session",
            "deploy in 2 hours",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    let cancel_response = harness
        .agent
        .handle_message(
            "test_session",
            "cancel",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();
    assert!(cancel_response.contains("cancelled"));

    let goals = harness
        .state
        .get_goals_for_session("test_session")
        .await
        .unwrap();
    assert_eq!(goals.len(), 1);
    assert_eq!(goals[0].status, "cancelled");
}

#[tokio::test]
async fn test_orchestration_targeted_cancel_text_does_not_auto_cancel_session_goal() {
    let provider = MockProvider::with_responses(vec![
        // Deterministic cancel detection classifies this as a targeted cancel,
        // which falls through to the normal loop instead of auto-cancelling.
        // The deferral below is bounced by the deferred-action gate.
        MockProvider::text_response("I'll look into which goal you mean."),
        // Deferred-action retry produces a real tool call
        MockProvider::tool_call_response("system_info", "{}"),
        MockProvider::text_response("Please share the goal ID to cancel that specific goal."),
    ]);
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let morning_goal = Goal::new_continuous(
        "Send me a slack message at 7:00 am EST tomorrow with a positive message",
        "test_session",
        Some(2000),
        Some(20000),
    );
    harness.state.create_goal(&morning_goal).await.unwrap();

    let english_goal = Goal::new_continuous(
        "English Research: Researching English pronunciation/phonetics for Spanish speakers",
        "other_session",
        Some(2000),
        Some(20000),
    );
    harness.state.create_goal(&english_goal).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "test_session",
            "cancel this goal: English Research: Researching English",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert_eq!(
        response,
        "Please share the goal ID to cancel that specific goal."
    );
    assert_eq!(
        harness.provider.call_count().await,
        3,
        "Targeted cancel text should not trigger session-wide auto-cancel shortcut"
    );

    let morning_after = harness
        .state
        .get_goal(&morning_goal.id)
        .await
        .unwrap()
        .unwrap();
    let english_after = harness
        .state
        .get_goal(&english_goal.id)
        .await
        .unwrap()
        .unwrap();
    assert_eq!(morning_after.status, "active");
    assert_eq!(english_after.status, "active");
}

#[tokio::test]
async fn test_orchestration_schedule_new_message_cancels_pending() {
    let provider = MockProvider::with_responses(vec![
        // Turn 1 ("deploy in 2 hours") routes via the deterministic schedule
        // heuristic without an LLM call. Turn 2 ("what is rust?") consumes
        // these; the answer is repeated in case the first short reply is
        // bounced as non-substantive.
        MockProvider::text_response("Rust is a systems programming language."),
        MockProvider::text_response("Rust is a systems programming language."),
    ]);
    let harness = setup_test_agent_orchestrator(provider).await.unwrap();

    let _ = harness
        .agent
        .handle_message(
            "test_session",
            "deploy in 2 hours",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    let _ = harness
        .agent
        .handle_message(
            "test_session",
            "what is rust?",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    let goals = harness
        .state
        .get_goals_for_session("test_session")
        .await
        .unwrap();
    assert_eq!(goals.len(), 1);
    assert_eq!(goals[0].status, "cancelled");
}

#[tokio::test]
async fn test_zero_tool_fabricated_mutation_claim_is_blocked() {
    // Reproduces the 2026-06-06 attribution-run turn-10 bug: the model
    // claimed "I have deleted the folder" without making a single tool
    // call, and the completion phase accepted it. A past-tense side-effect
    // claim in a zero-tool task with a mutation contract must be treated
    // like a deferred action: nudged with a hard tool requirement, never
    // accepted as the final answer.
    let fabrication = "I have deleted the folder /tmp/fab-test entirely.";
    let provider = MockProvider::with_responses(vec![
        MockProvider::text_response(fabrication),
        MockProvider::text_response(fabrication),
        MockProvider::text_response(
            "I could not verify the deletion because no command was run.",
        ),
    ]);
    let harness = setup_test_agent(provider).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "test_session",
            "Delete the folder /tmp/fab-test entirely.",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    // The fabricated claim must not survive as the final answer.
    assert!(
        !response.contains("I have deleted"),
        "fabricated zero-tool mutation claim was accepted as completion: {response}"
    );

    // The loop must have continued past the first reply, and the hard
    // tool-call requirement directive must have been injected.
    let calls = harness.provider.call_log.lock().await;
    assert!(
        calls.len() >= 2,
        "completion was accepted on the first iteration (calls={})",
        calls.len()
    );
    let nudged = calls.iter().skip(1).any(|c| {
        c.messages.iter().any(|m| {
            m["content"]
                .as_str()
                .is_some_and(|t| t.contains("MUST include at least one tool call"))
        })
    });
    assert!(
        nudged,
        "DeferredToolCallRequired directive was not injected after the fabricated claim"
    );

    let event_store = crate::events::EventStore::new(harness.state.pool())
        .await
        .expect("event store from harness pool");
    let events = event_store
        .query_recent_events("test_session", 200)
        .await
        .expect("recent events");
    let saw_mutation_gate_warning = events.iter().any(|event| {
        let Ok(data) = event.parse_data::<crate::events::DecisionPointData>() else {
            return false;
        };
        data.decision_type == crate::events::DecisionType::PostExecutionValidation
            && data
                .metadata
                .get("condition")
                .and_then(serde_json::Value::as_str)
                == Some("expects_mutation_gate_evaluated")
            && data
                .metadata
                .get("assistant_claimed_mutation")
                .and_then(serde_json::Value::as_bool)
                == Some(true)
            && data
                .metadata
                .get("mutation_tool_calls_count")
                .and_then(serde_json::Value::as_u64)
                == Some(0)
            && data
                .metadata
                .get("outcome")
                .and_then(serde_json::Value::as_str)
                == Some("blocked_claimed_mutation_without_tool")
    });
    assert!(
        saw_mutation_gate_warning,
        "mutation gate did not emit explicit warning telemetry for fabricated zero-tool mutation claim"
    );
}

#[tokio::test]
async fn test_zero_tool_fabricated_delegation_claim_is_blocked() {
    let fabrication =
        "I've initiated a deep analysis using a specialized review agent. I'll return shortly.";
    let provider = MockProvider::with_responses(vec![
        MockProvider::text_response(fabrication),
        MockProvider::text_response(
            "I could not start a specialist agent, so no delegated review is running.",
        ),
    ]);
    let harness = setup_test_agent(provider).await.unwrap();

    let response = harness
        .agent
        .handle_message(
            "fabricated_delegation",
            "Analyze that resume. Any flaws?",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();

    assert!(
        !response.contains("I've initiated"),
        "fabricated zero-tool delegation claim was accepted: {response}"
    );
    let calls = harness.provider.call_log.lock().await;
    assert!(calls.len() >= 2);
    assert!(calls.iter().skip(1).any(|call| {
        call.messages.iter().any(|message| {
            message["content"]
                .as_str()
                .is_some_and(|text| text.contains("MUST include at least one tool call"))
        })
    }));
}