Skip to content
Open
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
42 changes: 36 additions & 6 deletions extraction/commitments.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func (e *Extractor) ProcessBatch(ctx context.Context) error {
}

func (e *Extractor) extractFromChat(ctx context.Context, apiKey string, msgs []*store.Message, openCommitments []*store.Commitment) (*extractionResult, error) {
prompt := buildExtractionPrompt(msgs, openCommitments)
prompt := buildExtractionPrompt(msgs, openCommitments, e.db.GetTrackTasks())
model := e.db.GetModel()
response, err := callClaude(ctx, apiKey, model, prompt)
if err != nil {
Expand All @@ -256,28 +256,58 @@ func (e *Extractor) extractFromChat(ctx context.Context, apiKey string, msgs []*
return &result, nil
}

func buildExtractionPrompt(msgs []*store.Message, openCommitments []*store.Commitment) string {
func buildExtractionPrompt(msgs []*store.Message, openCommitments []*store.Commitment, trackTasks bool) string {
var sb strings.Builder
sb.WriteString(`Analyze these WhatsApp messages. Do two things:

1. EXTRACT NEW COMMITMENTS — promises, obligations, or things someone said they would do.
1. EXTRACT NEW COMMITMENTS — `)
if trackTasks {
sb.WriteString("promises, obligations, and direct tasks someone is expected to do.")
} else {
sb.WriteString("promises, obligations, or things someone said they would do.")
}
sb.WriteString(`
For each new commitment, return:
- title: short description of the commitment
- context: one sentence explaining the situation
- direction: "you_owe" if the user (messages marked [ME]) made the promise, "they_owe" if someone else did
- direction: `)
if trackTasks {
sb.WriteString(`"you_owe" if the user (messages marked [ME]) made the promise OR was directly asked/told to do something; "they_owe" if someone else promised the user or the user asked them to do something`)
} else {
sb.WriteString(`"you_owe" if the user (messages marked [ME]) made the promise, "they_owe" if someone else did`)
}
sb.WriteString(`
- source_quote: the exact message text that contains the commitment
- due_hint: any mentioned deadline or timeframe, converted to a concrete date/time if possible (e.g. "tomorrow" → "May 30", "by EOD" → "today evening"). Empty string if none
- person_name: the name of the other person involved

IMPORTANT — only extract REAL commitments. A commitment is someone explicitly stating they WILL do something, or agreeing to a request. Do NOT extract:
`)
if trackTasks {
sb.WriteString(`Two kinds of commitment count:
A) A PROMISE — someone explicitly states they WILL do something, or agrees to a request.
B) A DIRECT TASK — someone asks or tells another person to do a specific, actionable thing, addressed directly to them. This includes imperative requests in any language (e.g. "send me the file", "call them tomorrow", "kal bhej dena", "reply kar dena", "remind me", "book the tickets"). Capture these EVEN IF the recipient has not yet replied with agreement — the request itself creates something to track. A task someone asks the user to do is "you_owe"; a task the user asks someone else to do is "they_owe".

Do NOT extract:
- Pure questions seeking information ("what time is it?", "did you eat?") with no action requested
- Offers or suggestions ("I could...", "maybe we should...") — only extract if they clearly commit to or assign an action
- Greetings, small talk, reactions, or emotional messages
- Status updates or announcements that don't involve a task or a promise to act
- Rhetorical statements or vague intentions ("we should catch up sometime", "let's hang out soon") — these have no specific actionable task
- Messages in any language follow the same rules — translate mentally but apply the same standard

The bar for a DIRECT TASK is that it names a specific, actionable thing addressed to a particular person. When a request is that concrete, extract it. When it is vague, social, or merely informational, do not.`)
} else {
sb.WriteString(`IMPORTANT — only extract REAL commitments. A commitment is someone explicitly stating they WILL do something, or agreeing to a request. Do NOT extract:
- Questions ("can you...?", "would you mind...?") — these are requests, not commitments, unless answered with agreement
- Offers or suggestions ("I could...", "maybe we should...") — only extract if they clearly commit to action
- Greetings, small talk, reactions, or emotional messages
- Status updates or announcements that don't involve a promise to act
- Rhetorical statements or vague intentions ("we should catch up sometime")
- Messages in any language follow the same rules — translate mentally but apply the same strict standard

When in doubt, do NOT extract. False positives are worse than missed commitments.
When in doubt, do NOT extract. False positives are worse than missed commitments.`)
}
sb.WriteString(`

2. AUTO-RESOLVE — this is critical. Carefully check if ANY of the existing open commitments below have been fulfilled, completed, or made irrelevant by the new messages. Be aggressive about detecting resolution. Mark a commitment as resolved if:
- The promised action was done (sent a doc, made a call, shared info, etc.)
Expand Down
24 changes: 24 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ func (s *Server) registerRoutes() {
s.mux.HandleFunc("/api/user-name", s.requireAuth(s.handleUserName))
s.mux.HandleFunc("/api/setup/validate", s.requireAuth(s.handleValidateKey))
s.mux.HandleFunc("/api/model", s.requireAuth(s.handleModel))
s.mux.HandleFunc("/api/track-tasks", s.requireAuth(s.handleTrackTasks))
s.mux.HandleFunc("/api/setup/update-key", s.requireAuth(s.handleUpdateKey))
s.mux.HandleFunc("/api/debug", s.requireAuth(s.handleDebug))
s.mux.HandleFunc("/api/logout", s.requireAuth(s.handleLogout))
Expand Down Expand Up @@ -437,6 +438,29 @@ func (s *Server) handleModel(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]any{"ok": true, "model": body.Model})
}

func (s *Server) handleTrackTasks(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
writeJSON(w, map[string]bool{"track_tasks": s.db.GetTrackTasks()})
return
}
if r.Method != "POST" {
http.Error(w, "method not allowed", 405)
return
}
var body struct {
TrackTasks bool `json:"track_tasks"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "invalid body", 400)
return
}
if err := s.db.SetTrackTasks(body.TrackTasks); err != nil {
http.Error(w, "failed to save", 500)
return
}
writeJSON(w, map[string]any{"ok": true, "track_tasks": body.TrackTasks})
}

func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "method not allowed", 405)
Expand Down
63 changes: 63 additions & 0 deletions server/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,37 @@
font-family: monospace;
}

.toggle {
position: relative;
display: inline-block;
width: 38px;
height: 22px;
flex-shrink: 0;
}
.toggle input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
position: absolute;
inset: 0;
cursor: pointer;
background: var(--line);
border-radius: 999px;
transition: background 0.2s ease;
}
.toggle-slider::before {
content: "";
position: absolute;
height: 18px;
width: 18px;
left: 2px;
top: 2px;
background: #fff;
border-radius: 50%;
box-shadow: 0 1px 2px rgba(0,0,0,0.25);
transition: transform 0.2s ease;
}
.toggle input:checked + .toggle-slider { background: var(--accent); }
.toggle input:checked + .toggle-slider::before { transform: translateX(16px); }

/* Responsive */
@media (max-width: 640px) {
.topbar { padding: 10px 16px; }
Expand Down Expand Up @@ -1930,6 +1961,19 @@ <h4>Auto-resolves</h4>
</select>
</div>
</div>
<div class="settings-group">
<div class="settings-group-title">Extraction</div>
<div class="settings-row">
<span class="settings-row-label" style="display: flex; flex-direction: column; gap: 2px; padding-right: 12px;">
Track unanswered tasks
<span style="font-size: 11px; color: var(--ink-tertiary); font-weight: 400;">Also capture tasks someone asks you to do, even before you agree to them — not just explicit promises. Catches more, with more false positives.</span>
</span>
<label class="toggle">
<input type="checkbox" id="settings-track-tasks" onchange="updateTrackTasks()" />
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="settings-group">
<div class="settings-group-title">API Key</div>
<div class="settings-row" style="flex-direction: column; align-items: flex-start; gap: 8px;">
Expand Down Expand Up @@ -3143,6 +3187,7 @@ <h4>Auto-resolves</h4>
}
loadDebugInfo();
loadModelSetting();
loadTrackTasksSetting();
}
function closeSettings() { document.getElementById('settings-overlay').classList.remove('active'); }

Expand All @@ -3164,6 +3209,24 @@ <h4>Auto-resolves</h4>
} catch {}
}

async function loadTrackTasksSetting() {
try {
const d = await api('/api/track-tasks');
const cb = document.getElementById('settings-track-tasks');
if (cb) cb.checked = !!d.track_tasks;
} catch {}
}
async function updateTrackTasks() {
const cb = document.getElementById('settings-track-tasks');
try {
await api('/api/track-tasks', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({track_tasks: cb.checked})
});
} catch {}
}

async function loadDebugInfo() {
const panel = document.getElementById('debug-panel');
try {
Expand Down
16 changes: 16 additions & 0 deletions store/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,22 @@ func (db *DB) SetModel(model string) error {
return db.SetSetting("claude_model", model)
}

// Track tasks — when enabled, extraction also captures direct, unanswered
// tasks/requests (imperatives) as commitments, not just explicit promises.
// Off by default to preserve the strict, low-false-positive behavior.

func (db *DB) GetTrackTasks() bool {
return db.GetSetting("track_tasks") == "1"
}

func (db *DB) SetTrackTasks(on bool) error {
v := "0"
if on {
v = "1"
}
return db.SetSetting("track_tasks", v)
}

// API Key (encrypted at rest)

func (db *DB) GetAPIKey() string {
Expand Down