ldcon 0.2.0__py3-none-any.whl
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.
- ldcon/__init__.py +302 -0
- ldcon/_moved.py +49 -0
- ldcon/agent_tools.py +4 -0
- ldcon/agents/__init__.py +1 -0
- ldcon/agents/adjustment_guidance.py +73 -0
- ldcon/agents/adjustment_quality.py +399 -0
- ldcon/agents/agent_adapters.py +709 -0
- ldcon/agents/agent_background.py +307 -0
- ldcon/agents/agent_bundle.py +75 -0
- ldcon/agents/agent_cards.py +171 -0
- ldcon/agents/agent_config_access.py +22 -0
- ldcon/agents/agent_configuration.py +224 -0
- ldcon/agents/agent_configuration_policy.py +67 -0
- ldcon/agents/agent_connect.py +93 -0
- ldcon/agents/agent_device_parameters.py +41 -0
- ldcon/agents/agent_execution.py +77 -0
- ldcon/agents/agent_history.py +112 -0
- ldcon/agents/agent_inbox_watch.py +121 -0
- ldcon/agents/agent_interactive.py +386 -0
- ldcon/agents/agent_interactive_hooks.py +381 -0
- ldcon/agents/agent_language.py +48 -0
- ldcon/agents/agent_log_context.py +308 -0
- ldcon/agents/agent_log_read.py +250 -0
- ldcon/agents/agent_log_watch.py +27 -0
- ldcon/agents/agent_metrics.py +111 -0
- ldcon/agents/agent_monitor.py +272 -0
- ldcon/agents/agent_opencode_http.py +270 -0
- ldcon/agents/agent_opencode_mcp.py +64 -0
- ldcon/agents/agent_owner_status.py +216 -0
- ldcon/agents/agent_profiles.py +186 -0
- ldcon/agents/agent_providers.py +53 -0
- ldcon/agents/agent_repairs.py +105 -0
- ldcon/agents/agent_routing.py +90 -0
- ldcon/agents/agent_runtime.py +70 -0
- ldcon/agents/agent_sandbox.py +141 -0
- ldcon/agents/agent_startup_notice.py +142 -0
- ldcon/agents/agent_task.py +336 -0
- ldcon/agents/agent_tools.py +488 -0
- ldcon/agents/agent_window.py +70 -0
- ldcon/agents/agent_workspace.py +340 -0
- ldcon/agents/card_fallback.py +284 -0
- ldcon/agents/dlc_pi_adjustment.py +250 -0
- ldcon/agents/dlc_pi_plan.py +24 -0
- ldcon/agents/dlc_pi_response_plan.py +26 -0
- ldcon/agents/failure_policy.py +473 -0
- ldcon/agents/fallback_service.py +146 -0
- ldcon/agents/fallback_targets.py +96 -0
- ldcon/agents/gateway_role.py +31 -0
- ldcon/agents/manual_adjustment_grant.py +66 -0
- ldcon/agents/measurement_grant.py +95 -0
- ldcon/agents/monitor_quality_binding.py +33 -0
- ldcon/agents/notification_policy.py +346 -0
- ldcon/agents/operating_mode_client.py +99 -0
- ldcon/agents/report_pdf.py +105 -0
- ldcon/agents/resident_adjustment.py +572 -0
- ldcon/agents/resident_agent.py +438 -0
- ldcon/agents/resident_quality_apply.py +233 -0
- ldcon/agents/task_decisions.py +218 -0
- ldcon/agents/teams_card.py +100 -0
- ldcon/agents/teams_destinations.py +157 -0
- ldcon/cld_owner.py +4 -0
- ldcon/cli.py +19 -0
- ldcon/core/__init__.py +1 -0
- ldcon/core/adjustment_plan.py +47 -0
- ldcon/core/adjustment_registry.py +100 -0
- ldcon/core/agent_authority.py +903 -0
- ldcon/core/card_admission.py +15 -0
- ldcon/core/card_definition.py +14 -0
- ldcon/core/card_reporting.py +238 -0
- ldcon/core/configuration.py +357 -0
- ldcon/core/configuration_transaction.py +225 -0
- ldcon/core/contracts.py +86 -0
- ldcon/core/current_settings.py +86 -0
- ldcon/core/deck_yaml.py +45 -0
- ldcon/core/device_conditions.py +133 -0
- ldcon/core/execution_context.py +3 -0
- ldcon/core/instrument_client.py +190 -0
- ldcon/core/interface_cards.py +520 -0
- ldcon/core/interface_schema.py +640 -0
- ldcon/core/laser_assets.py +238 -0
- ldcon/core/metric_quantities.py +38 -0
- ldcon/core/modulation.py +107 -0
- ldcon/core/operation_registry.py +195 -0
- ldcon/core/paths.py +26 -0
- ldcon/core/period_context.py +118 -0
- ldcon/core/records.py +4 -0
- ldcon/core/result_reservations.py +469 -0
- ldcon/core/site_bindings.py +25 -0
- ldcon/core/state_layout.py +70 -0
- ldcon/core/target_policy.py +79 -0
- ldcon/core/telegraf.py +106 -0
- ldcon/core/telemetry.py +130 -0
- ldcon/core/temperature_settings.py +12 -0
- ldcon/dashboard_metrics.py +4 -0
- ldcon/device_registry.py +4 -0
- ldcon/drivers/__init__.py +1 -0
- ldcon/drivers/cld1015.py +296 -0
- ldcon/drivers/cld1015_config.py +31 -0
- ldcon/drivers/dlcpro.py +177 -0
- ldcon/drivers/dlcpro_analog.py +33 -0
- ldcon/drivers/dlcpro_console.py +133 -0
- ldcon/drivers/dlcpro_priority.py +46 -0
- ldcon/drivers/ecdl.py +334 -0
- ldcon/drivers/esp32_servo.py +187 -0
- ldcon/drivers/fizeau.py +114 -0
- ldcon/drivers/frequency_sweep.py +69 -0
- ldcon/drivers/influx_wavelength.py +70 -0
- ldcon/drivers/metric_sources.py +82 -0
- ldcon/drivers/native_dither_telemetry.py +50 -0
- ldcon/drivers/rigol_scope.py +126 -0
- ldcon/drivers/siglent.py +418 -0
- ldcon/drivers/staircase.py +155 -0
- ldcon/drivers/wsu_udp.py +164 -0
- ldcon/esp32_time_telemetry.py +4 -0
- ldcon/example_config.py +41 -0
- ldcon/fallback_service.py +4 -0
- ldcon/instrument_owner.py +4 -0
- ldcon/instrument_service.py +4 -0
- ldcon/interface_worker.py +4 -0
- ldcon/job_service.py +4 -0
- ldcon/jobs/__init__.py +1 -0
- ldcon/jobs/adaptive_pid.py +234 -0
- ldcon/jobs/adaptive_window_session.py +922 -0
- ldcon/jobs/adjustment_continuation.py +284 -0
- ldcon/jobs/axis_frequency.py +219 -0
- ldcon/jobs/axis_frequency_report.py +86 -0
- ldcon/jobs/background_parameter_session.py +142 -0
- ldcon/jobs/background_pid_session.py +447 -0
- ldcon/jobs/branch_maps.py +71 -0
- ldcon/jobs/branch_target_migration.py +33 -0
- ldcon/jobs/card_runtime.py +157 -0
- ldcon/jobs/change_measure.py +293 -0
- ldcon/jobs/change_measure_adapter.py +360 -0
- ldcon/jobs/change_measure_dlc.py +94 -0
- ldcon/jobs/change_measure_parameters.py +222 -0
- ldcon/jobs/change_measure_target.py +135 -0
- ldcon/jobs/current_cards.py +159 -0
- ldcon/jobs/current_devices.py +71 -0
- ldcon/jobs/dashboard_api.py +61 -0
- ldcon/jobs/device_parameters.py +426 -0
- ldcon/jobs/dlc_pi_measurement.py +151 -0
- ldcon/jobs/dlcpro_awg_analysis.py +77 -0
- ldcon/jobs/dlcpro_awg_card.py +116 -0
- ldcon/jobs/dlcpro_cards.py +240 -0
- ldcon/jobs/ecdl_branch_hop.py +175 -0
- ldcon/jobs/ecdl_branch_recovery.py +176 -0
- ldcon/jobs/ecdl_devices.py +306 -0
- ldcon/jobs/ecdl_locked_sweep_proxy.py +526 -0
- ldcon/jobs/ecdl_mapped_recovery.py +6 -0
- ldcon/jobs/ecdl_mode_map.py +469 -0
- ldcon/jobs/ecdl_mode_recovery.py +20 -0
- ldcon/jobs/ecdl_online.py +158 -0
- ldcon/jobs/ecdl_operating_point.py +403 -0
- ldcon/jobs/ecdl_pi_commission.py +161 -0
- ldcon/jobs/ecdl_recovery.py +25 -0
- ldcon/jobs/ecdl_recovery_plan.py +90 -0
- ldcon/jobs/ecdl_recovery_test.py +59 -0
- ldcon/jobs/ecdl_relock.py +7 -0
- ldcon/jobs/ecdl_response_study.py +369 -0
- ldcon/jobs/ecdl_steady.py +343 -0
- ldcon/jobs/ecdl_thermal_context.py +242 -0
- ldcon/jobs/ecdl_window_center.py +734 -0
- ldcon/jobs/emission_card.py +102 -0
- ldcon/jobs/fringe_sweep_reference.py +89 -0
- ldcon/jobs/interface_devices.py +122 -0
- ldcon/jobs/interface_worker.py +471 -0
- ldcon/jobs/job_service.py +125 -0
- ldcon/jobs/laser_live.py +270 -0
- ldcon/jobs/learned_settling.py +450 -0
- ldcon/jobs/lock_target.py +461 -0
- ldcon/jobs/locked_triangle.py +36 -0
- ldcon/jobs/measurement_acquisition.py +51 -0
- ldcon/jobs/measurement_analysis.py +59 -0
- ldcon/jobs/measurement_execution.py +40 -0
- ldcon/jobs/measurement_notice.py +78 -0
- ldcon/jobs/measurement_presets.py +52 -0
- ldcon/jobs/measurement_report.py +76 -0
- ldcon/jobs/measurement_statistics.py +22 -0
- ldcon/jobs/metric_cards.py +34 -0
- ldcon/jobs/metric_interface.py +100 -0
- ldcon/jobs/modulation_sweep.py +274 -0
- ldcon/jobs/native_background_session.py +91 -0
- ldcon/jobs/native_current_acquire.py +143 -0
- ldcon/jobs/native_current_card.py +328 -0
- ldcon/jobs/native_current_online.py +129 -0
- ldcon/jobs/native_ff_proxy.py +1361 -0
- ldcon/jobs/native_ff_sweep.py +802 -0
- ldcon/jobs/online_pid.py +178 -0
- ldcon/jobs/operating_point.py +1399 -0
- ldcon/jobs/operating_point_proxy.py +236 -0
- ldcon/jobs/operation_execution.py +126 -0
- ldcon/jobs/operation_history.py +419 -0
- ldcon/jobs/optical_broker.py +23 -0
- ldcon/jobs/pi_algorithms.py +30 -0
- ldcon/jobs/pi_response.py +239 -0
- ldcon/jobs/progress.py +63 -0
- ldcon/jobs/quality_baseline.py +411 -0
- ldcon/jobs/recovery_decisions.py +131 -0
- ldcon/jobs/recovery_history.py +152 -0
- ldcon/jobs/recovery_procedure.py +285 -0
- ldcon/jobs/recovery_reference.py +123 -0
- ldcon/jobs/recovery_stationary.py +138 -0
- ldcon/jobs/response_algorithms.py +47 -0
- ldcon/jobs/response_comparison.py +62 -0
- ldcon/jobs/response_record.py +90 -0
- ldcon/jobs/runtime_parameters.py +434 -0
- ldcon/jobs/settling_profile.py +140 -0
- ldcon/jobs/shot_analysis.py +1300 -0
- ldcon/jobs/simulation.py +127 -0
- ldcon/jobs/slow_control.py +208 -0
- ldcon/jobs/slow_current.py +88 -0
- ldcon/jobs/study_adoption.py +122 -0
- ldcon/jobs/study_execution.py +182 -0
- ldcon/jobs/sweep_ff_analysis.py +320 -0
- ldcon/jobs/sweep_ff_optimization.py +202 -0
- ldcon/jobs/sweep_ff_plan.py +166 -0
- ldcon/jobs/sweep_ff_report.py +254 -0
- ldcon/jobs/sweep_ff_transport.py +326 -0
- ldcon/jobs/sweep_tracking.py +424 -0
- ldcon/jobs/sweep_verdict.py +288 -0
- ldcon/jobs/target_response.py +638 -0
- ldcon/jobs/target_response_analysis.py +65 -0
- ldcon/jobs/target_response_control.py +215 -0
- ldcon/jobs/target_response_decisions.py +152 -0
- ldcon/jobs/target_response_experiment.py +110 -0
- ldcon/jobs/target_response_feedforward.py +130 -0
- ldcon/jobs/target_response_search.py +225 -0
- ldcon/jobs/target_response_shift.py +475 -0
- ldcon/jobs/target_response_thermal.py +113 -0
- ldcon/jobs/target_response_thermal_experiment.py +120 -0
- ldcon/jobs/target_sequence.py +166 -0
- ldcon/jobs/temperature_identification.py +86 -0
- ldcon/jobs/wavelength_history.py +17 -0
- ldcon/jobs/window_actions.py +641 -0
- ldcon/jobs/window_center.py +784 -0
- ldcon/jobs/window_control.py +130 -0
- ldcon/jobs/window_decisions.py +105 -0
- ldcon/jobs/window_sampling.py +66 -0
- ldcon/jobs/workflow_artifacts.py +73 -0
- ldcon/migration/__init__.py +810 -0
- ldcon/migration/migration_decks.py +239 -0
- ldcon/migration/migration_readiness.py +397 -0
- ldcon/migration/migration_relocation.py +283 -0
- ldcon/monitor_owner.py +4 -0
- ldcon/owner_telemetry.py +4 -0
- ldcon/owners/__init__.py +1 -0
- ldcon/owners/adc_log_threshold.py +209 -0
- ldcon/owners/background_grants.py +44 -0
- ldcon/owners/cld_instrument_owner.py +96 -0
- ldcon/owners/cld_owner.py +706 -0
- ldcon/owners/cld_projection.py +194 -0
- ldcon/owners/cld_system_owner.py +276 -0
- ldcon/owners/current_owner.py +68 -0
- ldcon/owners/device_registry.py +229 -0
- ldcon/owners/dlcpro_awg_plan.py +115 -0
- ldcon/owners/dlcpro_awg_port.py +98 -0
- ldcon/owners/dlcpro_awg_protocol.py +190 -0
- ldcon/owners/dlcpro_awg_return.py +63 -0
- ldcon/owners/dlcpro_awg_sweep.py +437 -0
- ldcon/owners/dlcpro_awg_timing.py +35 -0
- ldcon/owners/dlcpro_awg_tracking.py +175 -0
- ldcon/owners/dlcpro_awg_tracking_guard.py +87 -0
- ldcon/owners/dlcpro_awg_tracking_report.py +49 -0
- ldcon/owners/dlcpro_awg_zero.py +82 -0
- ldcon/owners/dlcpro_deployment.py +66 -0
- ldcon/owners/dlcpro_ff.py +64 -0
- ldcon/owners/dlcpro_ff_adjustment.py +159 -0
- ldcon/owners/dlcpro_ff_delta_adjustment.py +194 -0
- ldcon/owners/dlcpro_frequency_window.py +61 -0
- ldcon/owners/dlcpro_observation.py +62 -0
- ldcon/owners/dlcpro_owner.py +830 -0
- ldcon/owners/dlcpro_pi_protocol.py +44 -0
- ldcon/owners/dlcpro_pi_trial.py +161 -0
- ldcon/owners/dlcpro_readback.py +50 -0
- ldcon/owners/dlcpro_service.py +203 -0
- ldcon/owners/dlcpro_target_changes.py +330 -0
- ldcon/owners/dlcpro_target_step.py +44 -0
- ldcon/owners/dlcpro_target_store.py +75 -0
- ldcon/owners/dlcpro_telemetry.py +72 -0
- ldcon/owners/ecdl_background_parameters.py +248 -0
- ldcon/owners/ecdl_background_pid.py +184 -0
- ldcon/owners/ecdl_hot_control.py +255 -0
- ldcon/owners/ecdl_independent_pid.py +95 -0
- ldcon/owners/ecdl_locked_study.py +122 -0
- ldcon/owners/ecdl_locked_sweep.py +1875 -0
- ldcon/owners/ecdl_native_pid.py +181 -0
- ldcon/owners/ecdl_optical.py +37 -0
- ldcon/owners/ecdl_owner.py +1058 -0
- ldcon/owners/ecdl_policy_reload.py +494 -0
- ldcon/owners/ecdl_prelock.py +612 -0
- ldcon/owners/ecdl_projection.py +636 -0
- ldcon/owners/ecdl_steady_owner.py +1859 -0
- ldcon/owners/ecdl_steady_policy.py +212 -0
- ldcon/owners/ecdl_target_ff.py +214 -0
- ldcon/owners/ecdl_target_settling.py +163 -0
- ldcon/owners/ecdl_tec_pair.py +88 -0
- ldcon/owners/ecdl_thermal_study.py +251 -0
- ldcon/owners/emission.py +365 -0
- ldcon/owners/emission_adapters.py +268 -0
- ldcon/owners/esp32_native_client.py +335 -0
- ldcon/owners/esp32_native_owner.py +2166 -0
- ldcon/owners/esp32_time_telemetry.py +253 -0
- ldcon/owners/esp32_udp_gate.py +136 -0
- ldcon/owners/external_sweep_guard.py +44 -0
- ldcon/owners/flexible_native_trial.py +172 -0
- ldcon/owners/fringe_peaks.py +92 -0
- ldcon/owners/full_data_log.py +133 -0
- ldcon/owners/instrument_identity.py +125 -0
- ldcon/owners/instrument_owner.py +1041 -0
- ldcon/owners/instrument_projection.py +104 -0
- ldcon/owners/instrument_service.py +638 -0
- ldcon/owners/migration_physical_lock.py +162 -0
- ldcon/owners/monitor_owner.py +322 -0
- ldcon/owners/native_assembly.py +37 -0
- ldcon/owners/native_assembly_host.py +260 -0
- ldcon/owners/native_background.py +233 -0
- ldcon/owners/native_current_tuning.py +251 -0
- ldcon/owners/native_ff_proof.py +433 -0
- ldcon/owners/native_instrument_owner.py +161 -0
- ldcon/owners/native_modulation_limits.py +45 -0
- ldcon/owners/native_policy_reload.py +186 -0
- ldcon/owners/native_policy_safety.py +213 -0
- ldcon/owners/native_policy_update.py +473 -0
- ldcon/owners/native_system_owner.py +420 -0
- ldcon/owners/operating_mode_owner.py +128 -0
- ldcon/owners/owner_projection.py +530 -0
- ldcon/owners/owner_telemetry.py +166 -0
- ldcon/owners/pi_handover.py +154 -0
- ldcon/owners/projection_transport.py +143 -0
- ldcon/owners/remote_instruments.py +323 -0
- ldcon/owners/sdg_client.py +122 -0
- ldcon/owners/sdg_owner.py +468 -0
- ldcon/owners/tec_rate.py +29 -0
- ldcon/owners/wsu_udp_monitor.py +113 -0
- ldcon/report_pdf.py +4 -0
- ldcon/resources/agent-model-options.json +142 -0
- ldcon/resources/agent_contract/adjustment.txt +45 -0
- ldcon/resources/agent_contract/common.txt +42 -0
- ldcon/resources/agent_contract/diagnosis.txt +11 -0
- ldcon/resources/agent_contract/research.txt +20 -0
- ldcon/resources/agent_contract/response_optimization.txt +13 -0
- ldcon/resources/examples/README.html +10 -0
- ldcon/resources/examples/assignments/sr1092.yaml +100 -0
- ldcon/resources/examples/assignments/sr422.yaml +677 -0
- ldcon/resources/examples/assignments/thf685.yaml +7 -0
- ldcon/resources/examples/interface-decks/dfb-dbr-ldc-tec.yaml +430 -0
- ldcon/resources/examples/interface-decks/ecdl-dlcpro.yaml +288 -0
- ldcon/resources/examples/interface-decks/ecdl-pzt-ldc-tec.yaml +698 -0
- ldcon/resources/examples/lasers/sr1092.yaml +267 -0
- ldcon/resources/examples/lasers/sr422.yaml +403 -0
- ldcon/resources/examples/lasers/thf685.yaml +158 -0
- ldcon/resources/report_template/ldconreport.cls +42 -0
- ldcon/resources/report_template/report.tex +47 -0
- ldcon/web/__init__.py +1 -0
- ldcon/web/agent_approval_api.py +126 -0
- ldcon/web/agent_settings_api.py +54 -0
- ldcon/web/agent_task_api.py +211 -0
- ldcon/web/control_readback.py +63 -0
- ldcon/web/dashboard_activity.py +109 -0
- ldcon/web/dashboard_controls_revision.py +29 -0
- ldcon/web/dashboard_metrics.py +146 -0
- ldcon/web/dashboard_snapshot.py +163 -0
- ldcon/web/dashboard_state.py +104 -0
- ldcon/web/dashboard_wavelength.py +150 -0
- ldcon/web/device_parameters_api.py +52 -0
- ldcon/web/dither_quality.py +46 -0
- ldcon/web/dlcpro_target_api.py +91 -0
- ldcon/web/dlcpro_target_query.py +23 -0
- ldcon/web/failure_api.py +62 -0
- ldcon/web/gateway_lan.py +208 -0
- ldcon/web/grafana_activity.py +84 -0
- ldcon/web/grafana_dashboard.py +492 -0
- ldcon/web/grafana_dlcpro.py +158 -0
- ldcon/web/grafana_quality.py +167 -0
- ldcon/web/interface_projection.py +27 -0
- ldcon/web/job_registry.py +68 -0
- ldcon/web/job_report.py +122 -0
- ldcon/web/laser_operations.py +445 -0
- ldcon/web/lock_target_api.py +554 -0
- ldcon/web/lock_target_queue.py +212 -0
- ldcon/web/log_archive.py +191 -0
- ldcon/web/metric_query_profiles.py +378 -0
- ldcon/web/online_position.py +26 -0
- ldcon/web/operating_mode_api.py +72 -0
- ldcon/web/operation_api.py +34 -0
- ldcon/web/operation_observation.py +57 -0
- ldcon/web/operations_settings.py +92 -0
- ldcon/web/quality_threshold_metrics.py +64 -0
- ldcon/web/shot_analysis_api.py +136 -0
- ldcon/web/sweep_api.py +1186 -0
- ldcon/web/sweep_guard.py +45 -0
- ldcon/web/target_reference_panel.py +38 -0
- ldcon/web/teams_destinations_api.py +81 -0
- ldcon/web/web_fleet.py +482 -0
- ldcon/web/web_runtime.py +811 -0
- ldcon/web/web_server.py +402 -0
- ldcon/web/window_api.py +28 -0
- ldcon/web_assets/agent-settings.css +4 -0
- ldcon/web_assets/agent-settings.html +53 -0
- ldcon/web_assets/agent-settings.js +270 -0
- ldcon/web_assets/app.css +9 -0
- ldcon/web_assets/app.js +802 -0
- ldcon/web_assets/card-browser.js +160 -0
- ldcon/web_assets/card-guide.js +9 -0
- ldcon/web_assets/cards/acquire.metrics.html +82 -0
- ldcon/web_assets/cards/agent.history_review.html +13 -0
- ldcon/web_assets/cards/agent.log_watch.html +1 -0
- ldcon/web_assets/cards/agent.monitor.html +13 -0
- ldcon/web_assets/cards/agent.request.html +1 -0
- ldcon/web_assets/cards/agent.resident.html +1 -0
- ldcon/web_assets/cards/agent.task.html +17 -0
- ldcon/web_assets/cards/commission.ecdl_pi.html +1 -0
- ldcon/web_assets/cards/identify.axis_frequency.html +1 -0
- ldcon/web_assets/cards/identify.current_response.html +1 -0
- ldcon/web_assets/cards/laser.emission.html +8 -0
- ldcon/web_assets/cards/measure.batch.html +1 -0
- ldcon/web_assets/cards/measure.change.html +1 -0
- ldcon/web_assets/cards/measure.target_response.html +1 -0
- ldcon/web_assets/cards/monitor.wavelength.html +1 -0
- ldcon/web_assets/cards/notify.teams.html +1 -0
- ldcon/web_assets/cards/optimize.settling_criteria.html +1 -0
- ldcon/web_assets/cards/optimize.target_feedforward.html +1 -0
- ldcon/web_assets/cards/optimize.target_step.html +1 -0
- ldcon/web_assets/cards/optimize.target_thermal.html +1 -0
- ldcon/web_assets/cards/plan.ecdl_recovery.html +14 -0
- ldcon/web_assets/cards/prepare.ecdl_recovery.html +18 -0
- ldcon/web_assets/cards/recover.ecdl_mode_hop.html +12 -0
- ldcon/web_assets/cards/report.grafana_dashboard.html +22 -0
- ldcon/web_assets/cards/servo.current_pid.html +1 -0
- ldcon/web_assets/cards/servo.dlcpro_frequency.html +1 -0
- ldcon/web_assets/cards/servo.ecdl_steady_lock.html +11 -0
- ldcon/web_assets/cards/servo.native_current.html +1 -0
- ldcon/web_assets/cards/study.ecdl_current_response.html +1 -0
- ldcon/web_assets/cards/study.ecdl_locked_current_map.html +1 -0
- ldcon/web_assets/cards/study.ecdl_mode_map.html +6 -0
- ldcon/web_assets/cards/study.ecdl_thermal_context.html +5 -0
- ldcon/web_assets/cards/study.target_response.html +1 -0
- ldcon/web_assets/cards/sweep.current_modulation.html +1 -0
- ldcon/web_assets/cards/sweep.dlcpro_frequency.html +1 -0
- ldcon/web_assets/cards/sweep.servo_modulation.html +1 -0
- ldcon/web_assets/cards/test.ecdl_recovery.html +4 -0
- ldcon/web_assets/cards/tune.ecdl_initial_point.html +1 -0
- ldcon/web_assets/cards/tune.ecdl_operating_point.html +13 -0
- ldcon/web_assets/cards/tune.ecdl_window_center.html +7 -0
- ldcon/web_assets/cards/tune.native_current.html +1 -0
- ldcon/web_assets/cards/tune.pid_coordinate_search.html +1 -0
- ldcon/web_assets/cards/tune.pid_factor_reduction.html +1 -0
- ldcon/web_assets/cards/tune.pid_online.html +1 -0
- ldcon/web_assets/cards/tune.temperature_once.html +18 -0
- ldcon/web_assets/committing-input.js +35 -0
- ldcon/web_assets/configuration-editor.js +194 -0
- ldcon/web_assets/dashboard-state.js +39 -0
- ldcon/web_assets/failures.html +1 -0
- ldcon/web_assets/failures.js +19 -0
- ldcon/web_assets/fallback.css +3 -0
- ldcon/web_assets/fallback.html +1 -0
- ldcon/web_assets/fallback.js +21 -0
- ldcon/web_assets/fleet-settings.js +20 -0
- ldcon/web_assets/fleet.css +9 -0
- ldcon/web_assets/fleet.html +8 -0
- ldcon/web_assets/fleet.js +130 -0
- ldcon/web_assets/gateway-presentation.js +27 -0
- ldcon/web_assets/grafana-help.js +31 -0
- ldcon/web_assets/help.html +22 -0
- ldcon/web_assets/i18n.js +279 -0
- ldcon/web_assets/index.html +14 -0
- ldcon/web_assets/laser-workspace.js +279 -0
- ldcon/web_assets/modulation.html +1 -0
- ldcon/web_assets/modulation.js +14 -0
- ldcon/web_assets/reservations.html +16 -0
- ldcon/web_assets/reservations.js +77 -0
- ldcon/web_assets/teams-settings.js +91 -0
- ldcon/web_assets/window-actions.js +37 -0
- ldcon/web_assets/workspace.css +99 -0
- ldcon/web_fleet.py +4 -0
- ldcon/web_server.py +4 -0
- ldcon-0.2.0.dist-info/METADATA +85 -0
- ldcon-0.2.0.dist-info/RECORD +482 -0
- ldcon-0.2.0.dist-info/WHEEL +5 -0
- ldcon-0.2.0.dist-info/entry_points.txt +5 -0
- ldcon-0.2.0.dist-info/licenses/LICENSE +21 -0
- ldcon-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""Private, bounded event spool for an interactive Claude resident.
|
|
2
|
+
|
|
3
|
+
``initialize(root, session, cwd)`` creates one immutable connection identity and
|
|
4
|
+
returns its epoch. Never reuse a root for another connection. ``read_events``
|
|
5
|
+
atomically consumes the pending events in durable sequence order. The CLI reads
|
|
6
|
+
one hook JSON object from stdin and emits no hook decision or model context.
|
|
7
|
+
Only ``Stop.last_assistant_message`` is retained as text; prompts are hashed.
|
|
8
|
+
This module does not decide whether a turn completed or approve any operation.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
import fcntl
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import re
|
|
18
|
+
import stat
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
import uuid
|
|
22
|
+
|
|
23
|
+
from ldcon.agents.failure_policy import URL
|
|
24
|
+
|
|
25
|
+
EVENTS = frozenset({'SessionStart', 'UserPromptSubmit', 'Stop', 'StopFailure',
|
|
26
|
+
'SessionEnd', 'PostModelSwitch', 'ModelStatus'})
|
|
27
|
+
MAX_INPUT_BYTES = 1024 * 1024
|
|
28
|
+
MAX_PROMPT_BYTES = 262144
|
|
29
|
+
MAX_MESSAGE_BYTES = 32768
|
|
30
|
+
MAX_EVENTS = 64
|
|
31
|
+
MAX_STATE_BYTES = 8 * 1024 * 1024
|
|
32
|
+
LOCK_TIMEOUT_S = 5.
|
|
33
|
+
DIRECTORY_FLAGS = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
|
|
34
|
+
FILE_FLAGS = os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC
|
|
35
|
+
IDENTIFIER = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.-]{0,127}')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ProtocolError(RuntimeError):
|
|
39
|
+
"""The spool cannot safely represent or consume this connection."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _uuid(value):
|
|
43
|
+
try:
|
|
44
|
+
return isinstance(value, str) and str(uuid.UUID(value)) == value
|
|
45
|
+
except (ValueError, AttributeError):
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _directory(root, *, private=True):
|
|
50
|
+
"""Reject symlinks in every path component, including the private root."""
|
|
51
|
+
path = Path(os.path.abspath(root))
|
|
52
|
+
descriptor = os.open(path.anchor, DIRECTORY_FLAGS)
|
|
53
|
+
try:
|
|
54
|
+
for component in path.parts[1:]:
|
|
55
|
+
child = os.open(component, DIRECTORY_FLAGS, dir_fd=descriptor)
|
|
56
|
+
os.close(descriptor)
|
|
57
|
+
descriptor = child
|
|
58
|
+
info = os.fstat(descriptor)
|
|
59
|
+
if private and (info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) & 0o077):
|
|
60
|
+
raise ProtocolError('Hook spool directory must be private and owned by this user')
|
|
61
|
+
return descriptor
|
|
62
|
+
except BaseException:
|
|
63
|
+
os.close(descriptor)
|
|
64
|
+
raise
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _check_file(descriptor, limit):
|
|
68
|
+
info = os.fstat(descriptor)
|
|
69
|
+
if (not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or
|
|
70
|
+
info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) & 0o077):
|
|
71
|
+
raise ProtocolError('Hook spool file is not a private single-link regular file')
|
|
72
|
+
if info.st_size > limit:
|
|
73
|
+
raise ProtocolError('Hook spool file exceeds its size limit')
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@contextmanager
|
|
77
|
+
def _locked(root):
|
|
78
|
+
directory = _directory(root)
|
|
79
|
+
lock = None
|
|
80
|
+
try:
|
|
81
|
+
lock = os.open('lock', os.O_RDWR | os.O_CREAT | FILE_FLAGS, 0o600, dir_fd=directory)
|
|
82
|
+
_check_file(lock, 0)
|
|
83
|
+
deadline = time.monotonic() + LOCK_TIMEOUT_S
|
|
84
|
+
while True:
|
|
85
|
+
try:
|
|
86
|
+
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
87
|
+
break
|
|
88
|
+
except BlockingIOError:
|
|
89
|
+
if time.monotonic() >= deadline:
|
|
90
|
+
raise ProtocolError('Hook spool lock deadline exceeded')
|
|
91
|
+
time.sleep(.01)
|
|
92
|
+
yield directory
|
|
93
|
+
finally:
|
|
94
|
+
if lock is not None:
|
|
95
|
+
os.close(lock)
|
|
96
|
+
os.close(directory)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _load(directory):
|
|
100
|
+
descriptor = os.open('state.json', os.O_RDONLY | FILE_FLAGS, dir_fd=directory)
|
|
101
|
+
try:
|
|
102
|
+
_check_file(descriptor, MAX_STATE_BYTES)
|
|
103
|
+
with os.fdopen(descriptor, 'rb', closefd=False) as stream:
|
|
104
|
+
raw = stream.read(MAX_STATE_BYTES + 1)
|
|
105
|
+
if len(raw) > MAX_STATE_BYTES:
|
|
106
|
+
raise ProtocolError('Hook spool state exceeds its size limit')
|
|
107
|
+
state = json.loads(raw)
|
|
108
|
+
except (ValueError, UnicodeError) as error:
|
|
109
|
+
raise ProtocolError('Hook spool state is invalid') from error
|
|
110
|
+
finally:
|
|
111
|
+
os.close(descriptor)
|
|
112
|
+
if (not isinstance(state, dict) or state.get('schema_version') != 1 or
|
|
113
|
+
not _uuid(state.get('epoch')) or not _uuid(state.get('session_id')) or
|
|
114
|
+
not isinstance(state.get('cwd'), str) or not os.path.isabs(state['cwd']) or
|
|
115
|
+
type(state.get('next_sequence')) is not int or state['next_sequence'] < 1 or
|
|
116
|
+
type(state.get('faulted')) is not bool or
|
|
117
|
+
not isinstance(state.get('events'), list) or len(state['events']) > MAX_EVENTS):
|
|
118
|
+
raise ProtocolError('Hook spool state has an invalid shape')
|
|
119
|
+
previous = 0
|
|
120
|
+
for event in state['events']:
|
|
121
|
+
if (not isinstance(event, dict) or event.get('epoch') != state['epoch'] or
|
|
122
|
+
type(event.get('sequence')) is not int or
|
|
123
|
+
not previous < event['sequence'] < state['next_sequence']):
|
|
124
|
+
raise ProtocolError('Hook spool sequence or epoch is invalid')
|
|
125
|
+
previous = event['sequence']
|
|
126
|
+
return state
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _save(directory, state):
|
|
130
|
+
data = (json.dumps(state, ensure_ascii=False, separators=(',', ':')) + '\n').encode('utf-8')
|
|
131
|
+
if len(data) > MAX_STATE_BYTES:
|
|
132
|
+
raise ProtocolError('Hook spool state exceeds its size limit')
|
|
133
|
+
temporary = '.state-' + uuid.uuid4().hex
|
|
134
|
+
descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | FILE_FLAGS,
|
|
135
|
+
0o600, dir_fd=directory)
|
|
136
|
+
try:
|
|
137
|
+
with os.fdopen(descriptor, 'wb') as stream:
|
|
138
|
+
stream.write(data)
|
|
139
|
+
stream.flush()
|
|
140
|
+
os.fsync(stream.fileno())
|
|
141
|
+
os.replace(temporary, 'state.json', src_dir_fd=directory, dst_dir_fd=directory)
|
|
142
|
+
os.fsync(directory)
|
|
143
|
+
finally:
|
|
144
|
+
try:
|
|
145
|
+
os.unlink(temporary, dir_fd=directory)
|
|
146
|
+
except FileNotFoundError:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def initialize(root, session, cwd):
|
|
151
|
+
"""Create a connection spool; refuse replacing even a fully consumed spool."""
|
|
152
|
+
if not _uuid(session) or not isinstance(cwd, (str, Path)) or not os.path.isabs(cwd):
|
|
153
|
+
raise ValueError('Canonical session UUID and absolute workspace required')
|
|
154
|
+
cwd = os.path.abspath(cwd)
|
|
155
|
+
if not Path(cwd).is_dir():
|
|
156
|
+
raise ValueError('Existing workspace required')
|
|
157
|
+
root = Path(os.path.abspath(root))
|
|
158
|
+
parent = _directory(root.parent, private=False)
|
|
159
|
+
try:
|
|
160
|
+
try:
|
|
161
|
+
os.mkdir(root.name, mode=0o700, dir_fd=parent)
|
|
162
|
+
except FileExistsError:
|
|
163
|
+
pass
|
|
164
|
+
finally:
|
|
165
|
+
os.close(parent)
|
|
166
|
+
with _locked(root) as directory:
|
|
167
|
+
try:
|
|
168
|
+
os.stat('state.json', dir_fd=directory, follow_symlinks=False)
|
|
169
|
+
except FileNotFoundError:
|
|
170
|
+
pass
|
|
171
|
+
else:
|
|
172
|
+
raise ProtocolError('Hook spool already initialized; use a new connection directory')
|
|
173
|
+
state = dict(schema_version=1, epoch=str(uuid.uuid4()), session_id=session,
|
|
174
|
+
cwd=cwd, next_sequence=1, faulted=False, events=[])
|
|
175
|
+
_save(directory, state)
|
|
176
|
+
return state['epoch']
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _safe_message(message):
|
|
180
|
+
"""Bound retained prose and redact transport URLs/credential-like strings."""
|
|
181
|
+
raw = message.encode('utf-8')
|
|
182
|
+
bounded = raw[:MAX_MESSAGE_BYTES].decode('utf-8', errors='ignore')
|
|
183
|
+
bounded = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', bounded)
|
|
184
|
+
bounded = ''.join(c for c in bounded if c in '\n\t' or (ord(c) >= 32 and ord(c) != 127))
|
|
185
|
+
bounded = URL.sub('[URL redacted]', bounded)
|
|
186
|
+
bounded = re.sub(r'\b(?:Bearer|Basic)\s+[A-Za-z0-9+/=._-]+', '[authorization redacted]',
|
|
187
|
+
bounded, flags=re.I)
|
|
188
|
+
bounded = re.sub(r'\b(?:password|secret|token|api_key|authorization)\s*[:=]\s*'
|
|
189
|
+
r'(?:"[^"]*"|\x27[^\x27]*\x27|[^\s,;]+)', '[credential redacted]',
|
|
190
|
+
bounded, flags=re.I)
|
|
191
|
+
encoded = bounded.encode('utf-8')
|
|
192
|
+
return (encoded[:MAX_MESSAGE_BYTES].decode('utf-8', errors='ignore'),
|
|
193
|
+
len(raw) > MAX_MESSAGE_BYTES or len(encoded) > MAX_MESSAGE_BYTES)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _normalize(state, payload):
|
|
197
|
+
if not isinstance(payload, dict):
|
|
198
|
+
return None, 'invalid_input_shape'
|
|
199
|
+
kind = payload.get('hook_event_name')
|
|
200
|
+
if not isinstance(kind, str) or kind not in EVENTS:
|
|
201
|
+
return None, 'unsupported_event'
|
|
202
|
+
if payload.get('session_id') != state['session_id']:
|
|
203
|
+
return None, 'session_mismatch'
|
|
204
|
+
cwd = payload.get('cwd')
|
|
205
|
+
if not isinstance(cwd, str) or not os.path.isabs(cwd):
|
|
206
|
+
return None, 'cwd_mismatch'
|
|
207
|
+
try:
|
|
208
|
+
if not Path(cwd).resolve(strict=True).is_relative_to(Path(state['cwd']).resolve(strict=True)):
|
|
209
|
+
return None, 'cwd_mismatch'
|
|
210
|
+
except (OSError, ValueError, RuntimeError):
|
|
211
|
+
return None, 'cwd_mismatch'
|
|
212
|
+
if 'epoch' in payload and payload['epoch'] != state['epoch']:
|
|
213
|
+
return None, 'epoch_mismatch'
|
|
214
|
+
event = dict(event=kind, session_id=state['session_id'], cwd=state['cwd'])
|
|
215
|
+
prompt = payload.get('prompt_id')
|
|
216
|
+
if kind in ('UserPromptSubmit', 'Stop', 'StopFailure') and not _uuid(prompt):
|
|
217
|
+
return None, 'missing_or_invalid_prompt_id'
|
|
218
|
+
if prompt is not None:
|
|
219
|
+
if not _uuid(prompt):
|
|
220
|
+
return None, 'invalid_prompt_id'
|
|
221
|
+
event['prompt_id'] = prompt
|
|
222
|
+
if payload.get('agent_id'):
|
|
223
|
+
return None, 'unexpected_subagent_event'
|
|
224
|
+
if kind == 'UserPromptSubmit':
|
|
225
|
+
text = payload.get('prompt')
|
|
226
|
+
if not isinstance(text, str):
|
|
227
|
+
return None, 'invalid_prompt'
|
|
228
|
+
encoded = text.encode('utf-8')
|
|
229
|
+
if len(encoded) > MAX_PROMPT_BYTES:
|
|
230
|
+
return None, 'prompt_too_large'
|
|
231
|
+
event['prompt_sha256'] = hashlib.sha256(encoded).hexdigest()
|
|
232
|
+
# The interactive editor can normalize JSON whitespace. Retain a second
|
|
233
|
+
# digest only for a complete JSON object; never retain or partially
|
|
234
|
+
# extract its content. Duplicate keys/nonfinite values are ambiguous.
|
|
235
|
+
def unique_object(pairs):
|
|
236
|
+
result = {}
|
|
237
|
+
for key, value in pairs:
|
|
238
|
+
if key in result:
|
|
239
|
+
raise ValueError('Duplicate JSON key')
|
|
240
|
+
result[key] = value
|
|
241
|
+
return result
|
|
242
|
+
def reject_constant(value):
|
|
243
|
+
raise ValueError('Nonfinite JSON value')
|
|
244
|
+
try:
|
|
245
|
+
structured = json.loads(text, object_pairs_hook=unique_object,
|
|
246
|
+
parse_constant=reject_constant)
|
|
247
|
+
if isinstance(structured, dict):
|
|
248
|
+
canonical = json.dumps(structured, ensure_ascii=True, separators=(',', ':'),
|
|
249
|
+
sort_keys=True, allow_nan=False).encode('utf-8')
|
|
250
|
+
if len(canonical) <= MAX_PROMPT_BYTES:
|
|
251
|
+
event['prompt_json_sha256'] = hashlib.sha256(canonical).hexdigest()
|
|
252
|
+
except (ValueError, TypeError, RecursionError):
|
|
253
|
+
pass
|
|
254
|
+
if kind == 'Stop':
|
|
255
|
+
message = payload.get('last_assistant_message')
|
|
256
|
+
if not isinstance(message, str):
|
|
257
|
+
return None, 'missing_or_invalid_assistant_message'
|
|
258
|
+
event['last_assistant_message'], event['message_truncated'] = _safe_message(message)
|
|
259
|
+
event['stop_hook_active'] = payload.get('stop_hook_active') is True
|
|
260
|
+
event['background_work_present'] = bool(payload.get('background_tasks') or payload.get('session_crons'))
|
|
261
|
+
# Error details may contain credentials; retain only an identifier, never
|
|
262
|
+
# StopFailure's rendered API error in last_assistant_message.
|
|
263
|
+
for key in {'SessionStart': ('source', 'model'), 'ModelStatus': ('model',), 'StopFailure': ('error',),
|
|
264
|
+
'SessionEnd': ('reason',), 'PostModelSwitch': ('from_model', 'to_model')}.get(kind, ()):
|
|
265
|
+
value = payload.get(key)
|
|
266
|
+
if value is not None:
|
|
267
|
+
if not isinstance(value, str) or not IDENTIFIER.fullmatch(value):
|
|
268
|
+
return None, 'invalid_' + key
|
|
269
|
+
event[key] = value
|
|
270
|
+
mode = payload.get('permission_mode')
|
|
271
|
+
if mode is not None:
|
|
272
|
+
if mode not in ('default', 'plan', 'acceptEdits', 'auto', 'dontAsk', 'bypassPermissions'):
|
|
273
|
+
return None, 'invalid_permission_mode'
|
|
274
|
+
event['permission_mode'] = mode
|
|
275
|
+
effort = payload.get('effort')
|
|
276
|
+
if isinstance(effort, dict) and effort.get('level') in ('low', 'medium', 'high', 'xhigh', 'max'):
|
|
277
|
+
event['effort'] = dict(level=effort['level'])
|
|
278
|
+
return event, None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def record_model_status(root, payload):
|
|
282
|
+
"""Consume official statusLine metadata, not terminal text or model prose.
|
|
283
|
+
|
|
284
|
+
Keep only this session's model/effort. Repeated identical UI refreshes are
|
|
285
|
+
coalesced; distinct transitions remain ordered with the completion hooks.
|
|
286
|
+
Nothing here authorizes a tool or acknowledges a submitted request.
|
|
287
|
+
"""
|
|
288
|
+
if not isinstance(payload, dict):
|
|
289
|
+
return record_event(root, None, input_error='invalid_model_status')
|
|
290
|
+
model = payload.get('model')
|
|
291
|
+
model = model.get('id') if isinstance(model, dict) else None
|
|
292
|
+
if not isinstance(model, str) or not IDENTIFIER.fullmatch(model):
|
|
293
|
+
return record_event(root, None, input_error='invalid_status_model')
|
|
294
|
+
return record_event(root, dict(hook_event_name='ModelStatus',
|
|
295
|
+
session_id=payload.get('session_id'), cwd=payload.get('cwd'), model=model,
|
|
296
|
+
effort=payload.get('effort')))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def record_event(root, payload, *, input_error=None):
|
|
300
|
+
"""Durably record one allowlisted event, or a latched protocol error.
|
|
301
|
+
|
|
302
|
+
The returned record contains no submitted prompt. Once faulted, later hooks
|
|
303
|
+
cannot manufacture completion records. Overflow discards pending records in
|
|
304
|
+
favor of one explicit error, so a queued Stop cannot hide the overflow.
|
|
305
|
+
"""
|
|
306
|
+
with _locked(root) as directory:
|
|
307
|
+
state = _load(directory)
|
|
308
|
+
if state['faulted']:
|
|
309
|
+
return None
|
|
310
|
+
try:
|
|
311
|
+
event, reason = _normalize(state, payload) if input_error is None else (None, input_error)
|
|
312
|
+
except (UnicodeError, TypeError, ValueError):
|
|
313
|
+
event, reason = None, 'invalid_input_encoding'
|
|
314
|
+
if not reason and event['event'] == 'ModelStatus':
|
|
315
|
+
previous = state.get('last_model_status', {})
|
|
316
|
+
same = all(previous.get(k) == event.get(k) for k in ('model', 'effort'))
|
|
317
|
+
state['last_model_status'] = {**event, 'observed_unix_s': time.time()}
|
|
318
|
+
if same:
|
|
319
|
+
_save(directory, state)
|
|
320
|
+
return event
|
|
321
|
+
if len(state['events']) >= MAX_EVENTS:
|
|
322
|
+
reason = 'spool_overflow'
|
|
323
|
+
state['events'] = []
|
|
324
|
+
if reason:
|
|
325
|
+
kind = payload.get('hook_event_name') if isinstance(payload, dict) else None
|
|
326
|
+
event = dict(event='protocol_error', reason=reason,
|
|
327
|
+
source_event=kind if isinstance(kind, str) and kind in EVENTS else None,
|
|
328
|
+
session_id=state['session_id'], cwd=state['cwd'])
|
|
329
|
+
state['faulted'] = True
|
|
330
|
+
state['last_protocol_error'] = dict(reason=reason, source_event=event['source_event'])
|
|
331
|
+
event.update(epoch=state['epoch'], sequence=state['next_sequence'])
|
|
332
|
+
state['next_sequence'] += 1
|
|
333
|
+
state['events'].append(event)
|
|
334
|
+
_save(directory, state)
|
|
335
|
+
return event
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def read_events(root):
|
|
339
|
+
"""Consume queued records exactly once under the same writer lock.
|
|
340
|
+
|
|
341
|
+
Callers must inspect the whole batch for protocol_error before accepting a
|
|
342
|
+
completion. Failure after this consumption is an unknown turn, not replay.
|
|
343
|
+
"""
|
|
344
|
+
with _locked(root) as directory:
|
|
345
|
+
state = _load(directory)
|
|
346
|
+
events = state['events']
|
|
347
|
+
if events:
|
|
348
|
+
state['events'] = []
|
|
349
|
+
_save(directory, state)
|
|
350
|
+
return events
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def main(argv=None):
|
|
354
|
+
args = sys.argv[1:] if argv is None else argv
|
|
355
|
+
model_status = len(args) == 2 and args[1] == '--model-status'
|
|
356
|
+
if len(args) != 1 and not model_status:
|
|
357
|
+
print('Interactive hook requires one private spool directory', file=sys.stderr)
|
|
358
|
+
return 1
|
|
359
|
+
try:
|
|
360
|
+
raw = sys.stdin.buffer.read(MAX_INPUT_BYTES + 1)
|
|
361
|
+
if len(raw) > MAX_INPUT_BYTES:
|
|
362
|
+
record_event(args[0], None, input_error='input_too_large')
|
|
363
|
+
else:
|
|
364
|
+
try:
|
|
365
|
+
payload = json.loads(raw)
|
|
366
|
+
except (ValueError, UnicodeError, RecursionError):
|
|
367
|
+
record_event(args[0], None, input_error='invalid_input_json')
|
|
368
|
+
else:
|
|
369
|
+
if model_status:
|
|
370
|
+
record_model_status(args[0], payload)
|
|
371
|
+
else:
|
|
372
|
+
record_event(args[0], payload)
|
|
373
|
+
return 0
|
|
374
|
+
except (OSError, ValueError, ProtocolError):
|
|
375
|
+
# Never expose arbitrary input, filesystem paths, or authentication text.
|
|
376
|
+
print('Interactive hook spool unavailable', file=sys.stderr)
|
|
377
|
+
return 1
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
if __name__ == '__main__':
|
|
381
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Language policy shared by resident instructions and automated reviews."""
|
|
2
|
+
|
|
3
|
+
LANGUAGE_INSTRUCTIONS = (
|
|
4
|
+
'Use English for system instructions, internal operational responses, startup and '
|
|
5
|
+
'reconnection notices, automated reviews, and unsolicited system alerts. '
|
|
6
|
+
'When answering a human request or asking a follow-up about it, use the language '
|
|
7
|
+
'of the original human request, including requests relayed through Teams or '
|
|
8
|
+
'agentbridge. An explicit language preference in that request takes precedence. '
|
|
9
|
+
'Do not infer the human language from an English system wrapper, translated '
|
|
10
|
+
'instructions, logs, or the connector. An automated log review or card failure '
|
|
11
|
+
'without an originating human request is system work and uses English, even if '
|
|
12
|
+
'its configuration or evidence contains another language. Preserve the registered '
|
|
13
|
+
'[from: <your-registered-id>] Teams prefix in every language.'
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
TIME_INSTRUCTIONS = (
|
|
17
|
+
'Use the execution machine\'s current local timezone for human-readable timestamps '
|
|
18
|
+
'in replies, reports, Teams messages and automated reviews. Determine it from the '
|
|
19
|
+
'execution machine OS, not the browser, user device or an assumed UTC timezone. '
|
|
20
|
+
'Include the calendar date and timezone abbreviation or numeric UTC offset; apply '
|
|
21
|
+
'the offset appropriate to each event date, including daylight-saving transitions. '
|
|
22
|
+
'Convert UTC or epoch evidence for presentation while preserving the original '
|
|
23
|
+
'timestamps and storage format; never change measurement clocks, scheduling, '
|
|
24
|
+
'retention or machine timezone to implement this display policy. Label source UTC '
|
|
25
|
+
'explicitly if quoting it. Resolve ambiguous local times with the numeric offset. '
|
|
26
|
+
'If the machine timezone cannot be verified, say so rather than inventing it.'
|
|
27
|
+
)
|
|
28
|
+
LANGUAGE_INSTRUCTIONS += '\n' + TIME_INSTRUCTIONS
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
NOTIFICATION_INSTRUCTIONS = (
|
|
32
|
+
'Automatic Teams notices use short English summaries, normally three to five lines: '
|
|
33
|
+
'registered sender, event/outcome, dated lock and optical-quality state, and next action '
|
|
34
|
+
'or decision. Supply notice_json structured facts to teams_notify (event, event_id, '
|
|
35
|
+
'case_id or operation_id, summary, action, observed_at with offset, lock_state, '
|
|
36
|
+
'optical_quality and optional facts). Keep full evidence locally. Do not send raw JSON, '
|
|
37
|
+
'tracebacks, reasoning transcripts, long paths or invented attachments. The host applies '
|
|
38
|
+
'the current notification policy and trusted human-request origin; never choose reporting '
|
|
39
|
+
'granularity or pretend an automated review originated from a human. Healthy unchanged '
|
|
40
|
+
'checks are local only; intermediate progress may be grouped. Do not bypass grouping '
|
|
41
|
+
'with new request IDs. Human replies keep their original request language and correlation. '
|
|
42
|
+
'Missing dated state evidence means unverified, never locked. Optical degradation does '
|
|
43
|
+
'not imply frequency-lock loss. Supply exact questions and complete changes for approvals; '
|
|
44
|
+
'never truncate values, units, scope, uncertainty or the required decision. A review alert '
|
|
45
|
+
'alone grants no approval. Queued, HTTP-accepted and human-confirmed delivery are distinct. '
|
|
46
|
+
'Never replay unknown sends or old pending notices because policy/formatting changed.'
|
|
47
|
+
)
|
|
48
|
+
LANGUAGE_INSTRUCTIONS += '\n' + NOTIFICATION_INSTRUCTIONS
|