Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
c931360
feat(desktop): deliver retrieval-grounded answers to a question the u…
kodjima33 Aug 20, 2026
35b8544
feat(desktop): replay the director's retrieval second call through th…
kodjima33 Aug 20, 2026
bb4637f
fix(desktop): preserve benchmark fixture formatting; add DEBUG presen…
kodjima33 Aug 20, 2026
66bbe0b
fix(desktop): scope retrieval grounding to insight/suggest; integrate…
kodjima33 Aug 20, 2026
683d1bd
fix(desktop): register answer-question-from-retrieval in the director…
kodjima33 Aug 20, 2026
3cf6c90
feat(desktop): evaluate typed content mid-visit; enable answer-delive…
kodjima33 Aug 20, 2026
1e8ed2d
fix(desktop): dwell refresh repeats within one context instead of exh…
kodjima33 Aug 20, 2026
716699d
fix(desktop): key typed-content dwell refresh on keyboard activity, n…
kodjima33 Aug 20, 2026
f579f59
fix(desktop): a question the user is writing is a notify-worthy fact
kodjima33 Aug 20, 2026
4e8f813
fix(desktop): a re-asked question is a fresh request, not repetition
kodjima33 Aug 20, 2026
d639bae
fix(desktop): floor paraphrased user questions too
kodjima33 Aug 20, 2026
ed60336
fix(desktop): the answer-a-question exception explicitly outranks the…
kodjima33 Aug 20, 2026
bfd2bd0
fix(desktop): an answer must ride the lookup — never answer from prio…
kodjima33 Aug 20, 2026
22ead5a
fix(desktop): answer-the-question exceptions live inside the silence …
kodjima33 Aug 20, 2026
f7185e4
feat(desktop): forced retrieval for validated user-question facts
kodjima33 Aug 21, 2026
186d63b
fix(desktop): forced answer evaluation omits the recent-deliveries list
kodjima33 Aug 21, 2026
0019874
feat(desktop): attribute the citation the model omitted on forced-que…
kodjima33 Aug 21, 2026
8b27b28
perf(desktop): tighten dwell-refresh pacing to meet the 30s answer ta…
kodjima33 Aug 21, 2026
0cf8d7e
fix(desktop): entry evaluation of a dwell refresh needs a post-entry …
kodjima33 Aug 21, 2026
6011274
fix(desktop): strip inline citation tokens from visible notification …
kodjima33 Aug 21, 2026
2d8f8ca
chore(desktop): drop temporary dwell diagnostics; correct policy doc …
kodjima33 Aug 21, 2026
722fad2
chore: define FC-model-optional-step-drops-owed-answer
kodjima33 Aug 21, 2026
308df04
chore: stamp evidence PR on FC-model-optional-step-drops-owed-answer
kodjima33 Aug 21, 2026
2b0c4ce
chore(desktop): cover ContextDwellRefreshPolicy in the context-bucket…
kodjima33 Aug 21, 2026
a3bf7b2
fix(desktop): flow lint scans the relocated notification bridge actions
kodjima33 Aug 21, 2026
cba9e1a
fix(desktop): harden the answer path per adversarial review
kodjima33 Aug 21, 2026
49c0049
fix(desktop): a delivered answer consumes every question fact in the …
kodjima33 Aug 21, 2026
3a78782
fix(desktop): a question fact is consumed only by its cited answer
kodjima33 Aug 21, 2026
3530fdf
fix(desktop): a strong user asking verb is the question signal by itself
kodjima33 Aug 21, 2026
e11e0c1
fix(desktop): survive extraction's subject mangling and request parap…
kodjima33 Aug 21, 2026
7127ed1
chore(desktop): drop departure-eval diagnostics
kodjima33 Aug 21, 2026
7fad3a8
fix(desktop): a dwell refresh requires its fresh capture or aborts an…
kodjima33 Aug 21, 2026
dbb04fc
fix(desktop): catch 'A user wrote:' phrasing; one grace follow-up per…
kodjima33 Aug 21, 2026
2d8508a
fix(desktop): dwell capture guard covers same-app title switches
kodjima33 Aug 21, 2026
dcd2b89
test(desktop): update prompt goldens for the question carve-outs; scr…
kodjima33 Aug 21, 2026
ffdf437
fix(desktop): keep drafted question text out of the forced-lookup deb…
kodjima33 Aug 21, 2026
7894f24
fix(desktop): don't fire a dwell refresh inside a composing pause
kodjima33 Aug 21, 2026
b8a7e8f
fix(desktop): dwell refresh survives hung or refused capture chains
kodjima33 Aug 21, 2026
7d966ae
fix(desktop): dwell capture timeout that survives a cancellation-inse…
kodjima33 Aug 21, 2026
c245fde
fix(desktop): floor passive draft-subject question phrasings
kodjima33 Aug 21, 2026
45dffe8
fix(desktop): implied citations require exact identifier-token inters…
kodjima33 Aug 21, 2026
c387781
fix(desktop): extraction must re-report a re-asked question
kodjima33 Aug 21, 2026
68d85b4
fix(desktop): classify user-included questions; stale chains can't mo…
kodjima33 Aug 21, 2026
7b76aff
fix(desktop): user-question facts expire ten minutes after their comp…
kodjima33 Aug 21, 2026
99c9606
fix(desktop): classify compose-anchored artifact-subject asks; ban th…
kodjima33 Aug 21, 2026
4ec8ec6
fix(desktop): a typed question outranks candidate resurfacing; one re…
kodjima33 Aug 21, 2026
d435fa4
fix(desktop): structural quoted-question classification; rescue fires…
kodjima33 Aug 21, 2026
47c5e6c
fix(desktop): received-content marker guards every artifact-subject q…
kodjima33 Aug 21, 2026
2ec9d92
Merge remote-tracking branch 'origin/main' into kodjima33/director-re…
kodjima33 Aug 21, 2026
b06a2b9
fix(desktop): artifact-subject question gaps tolerate identifier dots
kodjima33 Aug 21, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"schema_version": 1,
"id": "FC-model-optional-step-drops-owed-answer",
"status": "open",
"violated_contract": "When the client already holds deterministic evidence that the user asked a question \u2014 a validated user-authored-question fact \u2014 every stage between that evidence and the delivered answer must run mechanically. Leaving any stage to the model's discretion (whether to request the lookup, whether to weigh the anti-repetition list, whether to copy the citation ref) turns delivery into a coin flip: each stage individually complies most of the time, and the product still drops the answer most of the time because the stages multiply. Discretion belongs only to the stage that owns judgment \u2014 whether the retrieved content actually answers the question.",
"canonical_prevention": "ContextDirectorRetrievalHop.forcedLookupQuery makes retrieval mandatory when a validated user-question fact exists (the fact statement is the query, the evaluation runs once with retrieved context attached, and the recent-deliveries list is omitted mechanically); ContextFactWritePolicy.isUserAuthoredQuestion floors the fact class deterministically at write time; ContextDirectorRetrievalHop.impliedCitations attributes the citation when the message provably carries retrieved content. Content matching nothing retrieved still dies at the grounding veto.",
"canonical_prevention_artifact": [
"desktop/macos/Desktop/Tests/ContextProactivityRetrievalHopTests.swift",
"desktop/macos/Desktop/Tests/ContextFactWritePolicyTests.swift"
],
"scope_hints": [
"desktop/macos/Desktop/Sources/ProactiveAssistants/**"
],
"evidence_prs": [
11976
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,31 @@
windowTitle: input.window,
frameNumber: 0,
captureTime: input.capturedAt)
let prompt = ContextProactivityPromptBuilder.directorStablePrompt(snapshot: snapshot)
let uncachedPrompt = ContextProactivityPromptBuilder.directorVolatilePrompt(
// A probe with retrieved items replays the *second* director call of a
// visit exactly as `performRetrievalHop` builds it: lookup instruction in
// the stable prompt, RETRIEVED CONTEXT section appended to the uncached
// suffix, and the lookup-enabled schema. Without them it stays the exact
// first-call replay it always was.
let retrievedSection =
input.retrieved.isEmpty
? nil
: ContextDirectorRetrievalHop.promptSection(
query: input.lookupQuery, items: input.retrieved)
let allowLookup = retrievedSection != nil
let prompt = ContextProactivityPromptBuilder.directorStablePrompt(
snapshot: snapshot, allowLookup: allowLookup)
let volatilePrompt = ContextProactivityPromptBuilder.directorVolatilePrompt(
tasks: input.tasks, frame: frame, recentDeliveries: input.recentDeliveries,
visitCount: input.visitCount)
let uncachedPrompt = retrievedSection.map { volatilePrompt + "\n\n" + $0 } ?? volatilePrompt
let cacheKey = ContextPromptCacheKey.director
let started = DispatchTime.now().uptimeNanoseconds
let result = try await completion(
ModelQoS.Proactivity.reasoningOperation,
prompt,
uncachedPrompt,
nil,
ContextProactivityEngine.schema,
ContextProactivityEngine.schema(allowLookup: allowLookup),
cacheKey,
800,
nil)
Expand All @@ -115,16 +128,69 @@
throw ContextBucketDirectorProbeError.invalidModelResponse
}

return [
let citedRefs = ContextDirectorRetrievalHop.partitionCitedRefs(decision.bucketEntryRefs)
let validRetrieved = ContextDirectorRetrievalHop.validatedRetrievedRefs(
citedRefs.retrieved, allowed: Set(input.retrieved.map(\.ref)))
// The engine validates citations against the store; the probe has no
// store, so a citation validates only when it appears verbatim in the
// supplied snapshot text — the same "cite only supplied refs" rule.
let suppliedText = ([input.frozen] + input.tail + input.validatedFacts).joined(separator: "\n")
let validBucketRefs = citedRefs.bucket.filter { suppliedText.contains($0) }
let validFactIDs = decision.factIDs.filter { suppliedText.contains($0) }
// The real grounding guard runs on the replayed decision, so a probe run
// exercises model -> citation validation -> grounding veto exactly as the
// engine chains them.
let groundingPermits =
decision.decision == "silence"
? false
: ContextDirectorGrounding.permitsNonSilence(
decision: decision.decision, entryRefs: validBucketRefs, factIDs: validFactIDs,
retrievedRefs: validRetrieved)
var output = [
"decision": String(decision.decision.prefix(32)),
"title": decision.title,
"message": decision.message,
"reasoning": decision.reasoning,
"bucket_entry_ref_count": "\(decision.bucketEntryRefs.count)",
"bucket_entry_ref_count": "\(citedRefs.bucket.count)",
"fact_ref_count": "\(decision.factIDs.count)",
"retrieved_ref_count": "\(validRetrieved.count)",
"lookup_query": decision.lookupQuery ?? "",
"grounding_permits": groundingPermits ? "true" : "false",
"model": ContextProactivityTelemetry.boundedProviderModel(result.providerModel),
"latency_ms": "\(max(0, latencyMs))",
]
// present=1 continues a grounding-permitted decision into the real
// presentation gate stack, completing the model -> grounding ->
// presentation chain the engine runs for an organic delivery.
if input.present {
guard groundingPermits else {
output["presentation"] = "not_attempted_grounding_veto_or_silence"
return output
}
let title = decision.title
let message = decision.message
let decisionType = decision.decision
let refDetail = (validBucketRefs + validRetrieved).joined(separator: ", ")
output["presentation"] = await MainActor.run {
guard let ownerID = RuntimeOwnerIdentity.currentOwnerId(), !ownerID.isEmpty else {
return "no_owner"
}
let context = FloatingBarNotificationContext(
sourceTitle: title,
assistantId: "context-director",
contextSummary: "probe replay",
detail: refDetail,
provenanceRef: "probe-present")
let presentation = NotificationService.shared.presentContextDirectorNotification(
ownerID: ownerID,
title: title,
message: message,
decisionType: decisionType,
context: context)
return "\(presentation)"
}
}
return output
}

private struct Input {
Expand All @@ -145,6 +211,15 @@
/// Optional. Lets a probe exercise the recent-delivery prompt section, which is
/// otherwise only reachable from the live ledger.
let recentDeliveries: [ContextBucketRecentDelivery]
/// Optional. Non-empty turns the replay into the visit's *second* director
/// call, with these items quoted in a RETRIEVED CONTEXT section.
let retrieved: [ContextRetrievedItem]
/// Optional. The lookup query echoed into the retrieved section; ignored
/// when `retrieved` is empty.
let lookupQuery: String
/// Optional. "1" continues a grounding-permitted decision into the real
/// presentation gate stack.
let present: Bool

init(params: [String: String]) throws {
bucketID = try Self.requiredString(params, key: "bucket_id", maxLength: 200)
Expand Down Expand Up @@ -183,6 +258,40 @@
recentDeliveries = try Self.optionalRecentDeliveryList(
params, key: "recent_deliveries",
maxCount: ContextBucketRecentDelivery.promptCap)
retrieved = try Self.optionalRetrievedList(
params, key: "retrieved",
maxCount: ContextDirectorRetrievalHop.maximumPromptItems)
lookupQuery = params["lookup_query"].map { String($0.prefix(200)) } ?? ""
present = params["present"] == "1"
}

/// Absent means "first-call replay", so existing probe callers keep their
/// exact prompt, schema, and behaviour.
private static func optionalRetrievedList(
_ params: [String: String],
key: String,
maxCount: Int
) throws -> [ContextRetrievedItem] {
guard let raw = params[key], !raw.isEmpty else { return [] }
guard let data = raw.data(using: .utf8),
let values = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]],
values.count <= maxCount
else {
throw ContextBucketDirectorProbeError.invalidParams(key)
}
return try values.map { value in
guard let ref = value["ref"] as? String, !ref.isEmpty, ref.count <= 200,
let preview = value["preview"] as? String, !preview.isEmpty, preview.count <= 2_400
else {
throw ContextBucketDirectorProbeError.invalidParams(key)
}
let title = (value["title"] as? String) ?? ""
let createdAt = value["created_at"] as? String
guard title.count <= 400, (createdAt?.count ?? 0) <= 40 else {
throw ContextBucketDirectorProbeError.invalidParams(key)
}
return ContextRetrievedItem(ref: ref, title: title, preview: preview, createdAt: createdAt)
}
}

/// Absent means "no recent deliveries", so existing probe callers keep their behaviour.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
func registerContextBucketDirectorProbe() {
register(
name: "probe_context_bucket_director",
summary: "Replay a synthetic context bucket through the director model boundary without delivery",
summary:
"Replay a synthetic context bucket through the director model boundary; present=1 continues a grounding-permitted decision into the real presentation gate stack",
params: [
"bucket_id", "version", "header", "frozen", "tail", "validated_facts", "tasks", "app", "window",
"captured_at", "notify_worthiness", "visit_count",
"captured_at", "notify_worthiness", "visit_count", "retrieved", "lookup_query", "present",
],
safety: "network_or_model",
sideEffects: [
"may call model/backend services",
"eligible replays consume the signed-in account's proactivity reasoning quota",
"does not write the database, change gates/settings, graduate tasks, or deliver notifications",
"does not write the database, change gates/settings, or graduate tasks",
"present=1 shows a real notification, subject to the same owner/toggle/throttle gates as an organic delivery; without it nothing is delivered",
]
) { params in
guard AppBuild.isNonProduction else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,52 @@ class AssistantCoordinator {
return true
}

/// Content-refresh transition for a long dwell whose on-screen content
/// changed (see `ContextDwellRefreshPolicy`): closes and reopens the ACTIVE
/// context through the ordinary visit machinery, so the departing frame —
/// which now contains what the user typed — gets extraction, departure
/// evaluation, and the fresh visit gets its normal entry evaluation. Every
/// quota, cooldown, dedup, and budget gate applies unchanged.
/// Returns the arriving visit's fence so the caller can capture a
/// post-entry frame BEFORE engaging the director: the entry evaluation only
/// grounds on frames captured at or after the visit began, and the
/// preview-skip path may not produce another full frame for a static screen.
func refreshActiveContextForDwell(
expectedApp: String, expectedWindowTitle: String?
) async -> ContextVisitFence? {
guard ContextBucketsFeature.isEnabled else { return nil }
guard let app = lastTrackedApp, let frame = lastTrackedFrame else { return nil }
// The dwell task is detached from its tick: if the user switched contexts
// while it awaited, refreshing would close the just-opened visit and
// extract the OLD context's frame into the NEW context's bucket.
guard app == expectedApp,
ContextDetection.normalizeWindowTitle(lastTrackedWindowTitle)
== ContextDetection.normalizeWindowTitle(expectedWindowTitle)
else { return nil }
guard !RewindSettings.shared.isAppExcluded(app) else { return nil }
// A refused same-context refresh must be DROPPED, not queued: begin()
// stores a refused request as pending, and finishContextTransition would
// replay it as a phantom switch back to this context after the in-flight
// real transition completes.
guard contextTransitionQueue.inFlight == nil else { return nil }
let request = ContextTransitionRequest(app: app, windowTitle: lastTrackedWindowTitle)
guard contextTransitionQueue.begin(request) else { return nil }
defer { finishContextTransition(request) }
do {
let transition = try await ContextVisitCoordinator.shared.transition(
toApp: app,
windowTitle: lastTrackedWindowTitle,
departingFrame: frame)
if transition.departingQualified, let departingFence = transition.departingFence {
Task { await ContextBucketRollupWriter.shared.extract(frame: frame, fence: departingFence) }
}
return transition.arrivingFence
} catch {
logError("Context buckets: content-refresh transition failed", error: error)
return nil
}
}

/// Releases a completed transition and schedules the latest context observed
/// during its persistence await. The follow-up runs on the main actor, so it
/// cannot race the coordinator's tracked state or start a second write in
Expand Down Expand Up @@ -274,6 +320,20 @@ class AssistantCoordinator {

// MARK: - Frame Tracking & Distribution

/// The app of the context currently tracked for switches; the dwell task
/// uses it to drop stale captures after an app switch.
var currentTrackedApp: String? { lastTrackedApp }

/// Whether the tracker still points at this exact context. The dwell task
/// guards every capture with it: an app-only check let a same-app tab/title
/// switch during the async capture overwrite the tracked frame with the
/// departed window's pixels, contaminating the active bucket.
func isTracking(app: String, windowTitle: String?) -> Bool {
lastTrackedApp == app
&& ContextDetection.normalizeWindowTitle(lastTrackedWindowTitle)
== ContextDetection.normalizeWindowTitle(windowTitle)
}

/// Keep the latest frame reference fresh (call on every capture, even during delay).
func trackFrame(_ frame: CapturedFrame) {
lastTrackedFrame = frame
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation
import os

/// First-result-wins race for awaits that may never return.
///
/// `withTaskGroup` cannot bound a non-cooperative child: leaving the group
/// awaits every child, cancelled or not, so a stalled cancellation-insensitive
/// call (SCShareableContent resolution is one) blocks the caller indefinitely.
/// This race resumes on whichever branch finishes first and ABANDONS the
/// other — the loser keeps running in the background and its result is
/// dropped. Use only where an orphaned straggler is acceptable.
enum AsyncFirstResolved {
static func run<T: Sendable>(
_ a: @escaping @Sendable () async -> T?,
_ b: @escaping @Sendable () async -> T?
) async -> T? {
let resumed = OSAllocatedUnfairLock(initialState: false)
return await withCheckedContinuation { continuation in
let finish: @Sendable (T?) -> Void = { value in
let first = resumed.withLock { state -> Bool in
if state { return false }
state = true
return true
}
if first { continuation.resume(returning: value) }
}
Task { finish(await a()) }
Task { finish(await b()) }
}
}
}
Loading
Loading