Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion backend/crates/itcy/src/slack/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1095,7 +1095,14 @@ Status: **published**.",
Ok(s) => s,
Err(e) => return format!("Could not open draft store: {e}"),
};
match rework_stored_draft(&self.llm, &stored, instructions, Some(self.tools.as_ref())).await
match rework_stored_draft(
&self.llm,
&stored,
instructions,
Some(self.tools.as_ref()),
&self.tools.handles_index(),
)
.await
{
Ok(rew) => {
let mut row = stored_from_payload(DraftPayload {
Expand Down
9 changes: 8 additions & 1 deletion backend/crates/itcy/src/slack/tweets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,14 @@ Status: **published**.",
Ok(s) => s,
Err(e) => return format!("Could not open draft store: {e}"),
};
match rework_stored_tweet(&self.llm, &stored, instructions, Some(self.tools.as_ref())).await
match rework_stored_tweet(
&self.llm,
&stored,
instructions,
Some(self.tools.as_ref()),
&self.tools.handles_index(),
)
.await
{
Ok(rew) => {
let mut row = stored_from_payload(DraftPayload {
Expand Down
66 changes: 64 additions & 2 deletions backend/crates/itcy/src/sources/handles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ pub fn resolve_handles_path() -> Option<PathBuf> {

/// Load the handle registry. Returns an empty index when the file is not found.
///
/// Prefer passing [`ItcyTools::handles_index`] at runtime when tools are wired
/// (includes `/handle_add` hot reload without restart).
///
/// # Errors
///
/// Returns [`HandlesError`] when the file exists but cannot be read or parsed.
Expand Down Expand Up @@ -810,8 +813,10 @@ fn ensure_named_handle_in_body(
HandleMatch::X => e.x.eq_ignore_ascii_case(handle),
});
if let Some(entry) = entry {
if let Some((start, end)) = find_phrase_outside_url(body, &entry.name) {
return replace_range(body, start, end, handle);
for label in name_labels_for_body_match(&entry.name) {
if let Some((start, end)) = find_phrase_outside_url(body, &label) {
return replace_range(body, start, end, handle);
}
}
}
let trimmed = body.trim_start();
Expand All @@ -831,6 +836,17 @@ fn ensure_named_handle_in_body(
}
}

fn name_labels_for_body_match(name: &str) -> Vec<String> {
let name = name.trim();
let mut out = vec![name.to_string()];
if let Some(first) = name.split_whitespace().next() {
if first != name && first.len() >= 5 {
out.push(first.to_string());
}
}
out
}

fn use_publisher_name_lead(entry: &HandleEntry) -> bool {
if entry.linkedin_url.contains("/company/") {
return true;
Expand Down Expand Up @@ -927,6 +943,9 @@ fn upsert_handles_line(pack: &str, line: &str) -> String {
fn find_phrase_outside_url(hay: &str, phrase: &str) -> Option<(usize, usize)> {
let hay_l = hay.to_ascii_lowercase();
let needle = phrase.to_ascii_lowercase();
if needle.len() < 5 && !needle.contains(' ') {
return None;
}
let mut from = 0usize;
while from < hay_l.len() {
let Some(rel) = hay_l.get(from..).and_then(|s| s.find(needle.as_str())) else {
Expand Down Expand Up @@ -1117,6 +1136,49 @@ mod tests {
assert!(!named.contains("Isaac Sacolick"));
}

#[test]
fn short_handle_names_do_not_match_inside_common_phrases() {
let mut idx = HandlesIndex::default();
idx.upsert_entry(HandleEntry {
name: "code".into(),
linkedin: String::new(),
x: "@code".into(),
linkedin_url: String::new(),
x_url: "https://x.com/code".into(),
});
let pack = "handles: x=@code\n";
let body = "25k code reviews weekly on the platform.";
let out = ensure_x_handle_from_pack(body, pack, &idx);
assert!(
!out.contains("@code"),
"short name 'code' must not tag inside 'code reviews': {out}"
);
}

#[test]
fn vs_code_handle_does_not_tag_code_reviews() {
let mut idx = HandlesIndex::default();
idx.upsert_entry(HandleEntry {
name: "VS Code".into(),
linkedin: String::new(),
x: "@code".into(),
linkedin_url: String::new(),
x_url: "https://x.com/code".into(),
});
idx.upsert_entry(HandleEntry {
name: "DoorDash @DoorDash".into(),
linkedin: "@doordash".into(),
x: "@DoorDash".into(),
linkedin_url: "https://www.linkedin.com/company/doordash/".into(),
x_url: "https://x.com/DoorDash".into(),
});
let pack = "subject: DoorDash Flux\nhandles: x=@DoorDash\n";
let body = "🚀 DoorDash shifted 130k tasks. 25k code reviews weekly.";
let out = ensure_x_handle_from_pack(body, pack, &idx);
assert!(out.contains("@DoorDash"), "{out}");
assert!(!out.contains("@code"), "{out}");
}

#[test]
fn tweet_body_gets_x_handle_not_linkedin() {
let idx = isaac_index();
Expand Down
13 changes: 7 additions & 6 deletions backend/crates/itcy/src/sources/rework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::sources::draft_footer::{
compose_draft_message, ensure_primary_link_line, pick_link_options,
};
use crate::sources::draft_url::{extract_in_post_url, promote_link_option, set_single_in_post_url};
use crate::sources::handles::HandlesIndex;
use crate::sources::tweet_farce::{ensure_farce_mentions, stored_is_farce};
use crate::sources::tweet_footer::{
aerate_tweet_commentary, coerce_tweet_body, compose_tweet_message, ensure_operator_https_lines,
Expand Down Expand Up @@ -75,6 +76,7 @@ pub async fn rework_stored_draft(
stored: &StoredDraft,
instructions: &str,
tools: Option<&dyn ToolProvider>,
handles: &HandlesIndex,
) -> Result<ReworkedDraft, ReworkError> {
if stored.status != "open" {
return Err(ReworkError::NotOpen(
Expand All @@ -100,9 +102,8 @@ pub async fn rework_stored_draft(
let prose = crate::llm::sanitize_itcy_text(
&crate::sources::draft_footer::draft_prose_for_rework(&stored.body),
);
let handles = crate::sources::handles::load_handles().unwrap_or_default();
let brief_for_handles = format!("{}\n{instructions}\n{prose}", stored.subject);
crate::sources::handles::apply_brief_handles_to_pack(&mut pack, &brief_for_handles, &handles);
crate::sources::handles::apply_brief_handles_to_pack(&mut pack, &brief_for_handles, handles);
let user = draft_rework_user_message(
instructions,
&stored.draft_id,
Expand Down Expand Up @@ -150,7 +151,7 @@ pub async fn rework_stored_draft(
.await;
body = strip_leading_draft_id(&body);
body = crate::sources::handles::ensure_linkedin_brand_mention(&body);
body = crate::sources::handles::ensure_linkedin_handle_from_pack(&body, &pack, &handles);
body = crate::sources::handles::ensure_linkedin_handle_from_pack(&body, &pack, handles);
let body = compose_draft_message(&body, &stored.draft_id, &link_options);
let body = with_disclosure(&body, &trace);
info!(
Expand Down Expand Up @@ -328,6 +329,7 @@ pub async fn rework_stored_tweet(
stored: &StoredDraft,
instructions: &str,
tools: Option<&dyn ToolProvider>,
handles: &HandlesIndex,
) -> Result<ReworkedDraft, ReworkError> {
if stored.status != "open" {
return Err(ReworkError::NotOpen(
Expand All @@ -342,11 +344,10 @@ pub async fn rework_stored_tweet(
} else {
stored.research_pack.clone()
};
let handles = crate::sources::handles::load_handles().unwrap_or_default();
crate::sources::handles::apply_brief_handles_to_pack(
&mut pack,
&format!("{}\n{instructions}", stored.subject),
&handles,
handles,
);
let current = stored.link_options.first().cloned().unwrap_or_else(|| {
crate::sources::draft_url::extract_in_post_url(&stored.body).unwrap_or_default()
Expand Down Expand Up @@ -374,7 +375,7 @@ pub async fn rework_stored_tweet(
if farce {
body = ensure_farce_mentions(&body);
}
body = crate::sources::handles::ensure_x_handle_from_pack(&body, &pack, &handles);
body = crate::sources::handles::ensure_x_handle_from_pack(&body, &pack, handles);
let pack_urls = stored.sources.clone();
let (body, link_options) =
finalize_rework_tweet_output(body, stored, &pack_urls, &current, instructions, farce);
Expand Down
9 changes: 9 additions & 0 deletions backend/crates/itcy/src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ impl ItcyTools {
.clone()
}

/// Registry for draft/tweet paths: in-memory (hot `/handle_add`) when tools exist.
#[must_use]
pub fn runtime_handles(tools: Option<&Self>) -> HandlesIndex {
tools.map_or_else(
|| crate::sources::handles::load_handles().unwrap_or_default(),
Self::handles_index,
)
}

/// Parse + append `handles.toml` + hot-reload memory (no process restart).
///
/// # Errors
Expand Down
9 changes: 8 additions & 1 deletion backend/handles.toml
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ x = "@david_dossett"
x_url = "https://x.com/david_dossett"

[[handle]]
name = "code"
name = "VS Code"
x = "@code"
x_url = "https://x.com/code"

Expand Down Expand Up @@ -7564,3 +7564,10 @@ linkedin = "@scylladb"
x = "@ScyllaDB"
linkedin_url = "https://www.linkedin.com/company/scylladb/"
x_url = "https://x.com/ScyllaDB"

[[handle]]
name = "DoorDash @DoorDash"
linkedin = "@doordash"
x = "@DoorDash"
linkedin_url = "https://www.linkedin.com/company/doordash/"
x_url = "https://x.com/DoorDash"
35 changes: 30 additions & 5 deletions scripts/lib/twitter-brave-lock.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,40 @@
# One Brave CDP session at a time; pick a free debugging port (never steal :9224).
# shellcheck shell=bash

twitter_brave_stale_lock_pid() {
local lock="${1:?}"
[[ -f "${lock}" ]] || return 1
local pid
pid="$(tr -dc '0-9' <"${lock}" 2>/dev/null || true)"
[[ -n "${pid}" ]] || return 1
kill -0 "${pid}" 2>/dev/null
}

twitter_brave_clear_stale_lock() {
local lock="${1:?}"
if [[ -f "${lock}" ]] && ! twitter_brave_stale_lock_pid "${lock}"; then
rm -f "${lock}"
fi
}

twitter_brave_acquire_lock() {
local run_root="${1:?run root}"
local wait_secs="${2:-180}"
mkdir -p "${run_root}"
local lock="${run_root}/brave.lock"
exec 9>"${lock}"
if ! flock -n 9; then
echo "another X Brave session holds ${lock}; wait or stop the other ship/pulse/status run" >&2
return 1
fi
local deadline=$((SECONDS + wait_secs))
while (( SECONDS < deadline )); do
twitter_brave_clear_stale_lock "${lock}"
exec 9>"${lock}"
if flock -n 9; then
echo "$$" >&9
return 0
fi
exec 9>&- || true
sleep 2
done
echo "another X Brave session holds ${lock}; wait or stop the other ship/pulse/status run" >&2
return 1
}

twitter_brave_pick_cdp_port() {
Expand Down
Loading