ofiere-openclaw-plugin 4.46.0 → 4.47.0
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.
- package/package.json +1 -1
- package/src/staffPersona.ts +3 -1
- package/src/tools.ts +249 -112
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.47.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin for Ofiere PM - 16 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, SOP management, agent brain, talent management, and corporate frameworks",
|
|
6
6
|
"keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
|
package/src/staffPersona.ts
CHANGED
|
@@ -74,6 +74,7 @@ export interface DispatchContextRow {
|
|
|
74
74
|
attached_sop_ids: string[];
|
|
75
75
|
attached_framework_ids: string[];
|
|
76
76
|
task_id: string | null;
|
|
77
|
+
conversation_id: string | null;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
export async function loadDispatchContextBySession(
|
|
@@ -84,7 +85,7 @@ export async function loadDispatchContextBySession(
|
|
|
84
85
|
if (!sessionKey) return null;
|
|
85
86
|
const { data, error } = await supabase
|
|
86
87
|
.from("dispatch_context")
|
|
87
|
-
.select("subagent_id, attached_sop_ids, attached_framework_ids, task_id")
|
|
88
|
+
.select("subagent_id, attached_sop_ids, attached_framework_ids, task_id, conversation_id")
|
|
88
89
|
.eq("user_id", userId)
|
|
89
90
|
.eq("session_key", sessionKey)
|
|
90
91
|
.order("created_at", { ascending: false })
|
|
@@ -100,6 +101,7 @@ export async function loadDispatchContextBySession(
|
|
|
100
101
|
? (data.attached_framework_ids as unknown[]).filter((x): x is string => typeof x === "string")
|
|
101
102
|
: [],
|
|
102
103
|
task_id: (data.task_id as string | null) ?? null,
|
|
104
|
+
conversation_id: (data.conversation_id as string | null) ?? null,
|
|
103
105
|
};
|
|
104
106
|
}
|
|
105
107
|
|
package/src/tools.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
registerAttachmentContextHook,
|
|
20
20
|
} from "./attachments.js";
|
|
21
21
|
import { invalidateAgentTier } from "./agent-tier.js";
|
|
22
|
-
import { loadSubagentRow, readDispatchSubagentId } from "./staffPersona.js";
|
|
22
|
+
import { loadSubagentRow, readDispatchSubagentId, loadDispatchContextBySession, type DispatchContextRow } from "./staffPersona.js";
|
|
23
23
|
|
|
24
24
|
// ─── Tool result shape (matches OpenClaw SDK) ────────────────────────────────
|
|
25
25
|
|
|
@@ -458,12 +458,145 @@ async function handleListTasks(
|
|
|
458
458
|
}
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
+
// Cycle 7b BUGSHOOT-2 (BUG 5) — IANA timezone offset in hours. Works regardless
|
|
462
|
+
// of the VPS clock TZ because Intl.DateTimeFormat resolves the IANA zone
|
|
463
|
+
// directly without going through `new Date(local-string)` parsing.
|
|
464
|
+
function getTimezoneOffsetHours(tz: string): number {
|
|
465
|
+
try {
|
|
466
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
467
|
+
timeZone: tz,
|
|
468
|
+
timeZoneName: "shortOffset",
|
|
469
|
+
}).formatToParts(new Date());
|
|
470
|
+
const offsetPart = parts.find((p) => p.type === "timeZoneName");
|
|
471
|
+
const txt = offsetPart?.value || "GMT+0";
|
|
472
|
+
const m = txt.match(/GMT([+-]\d{1,2})(?::?(\d{2}))?/i);
|
|
473
|
+
if (!m) return 0;
|
|
474
|
+
const hours = parseInt(m[1], 10);
|
|
475
|
+
const minutes = m[2] ? parseInt(m[2], 10) : 0;
|
|
476
|
+
return hours + (hours < 0 ? -minutes / 60 : minutes / 60);
|
|
477
|
+
} catch {
|
|
478
|
+
return 7;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Cycle 7b BUGSHOOT-2 (BUG 5) — load the caller's IANA timezone from their
|
|
483
|
+
// profile row. The dashboard writes it on sign-in (browser-detected). Falls
|
|
484
|
+
// back to the plugin-config default if unset/missing.
|
|
485
|
+
async function loadUserTimezone(
|
|
486
|
+
supabase: SupabaseClient,
|
|
487
|
+
userId: string,
|
|
488
|
+
fallback: string,
|
|
489
|
+
): Promise<string> {
|
|
490
|
+
try {
|
|
491
|
+
const { data } = await supabase
|
|
492
|
+
.from("profiles")
|
|
493
|
+
.select("timezone")
|
|
494
|
+
.eq("id", userId)
|
|
495
|
+
.maybeSingle();
|
|
496
|
+
const tz = typeof data?.timezone === "string" && data.timezone.trim()
|
|
497
|
+
? data.timezone.trim()
|
|
498
|
+
: null;
|
|
499
|
+
return tz || fallback;
|
|
500
|
+
} catch {
|
|
501
|
+
return fallback;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Cycle 7b BUGSHOOT-2 (BUG 5) — normalize a start_date string to UTC against
|
|
506
|
+
// the user's IANA timezone. Returns the canonical UTC ISO string, the epoch
|
|
507
|
+
// for next_run_at, and the local clock components for scheduler_events.
|
|
508
|
+
//
|
|
509
|
+
// Three input shapes are handled:
|
|
510
|
+
// - explicit UTC marker (Z, +00, +0000, +00:00, UTC) → no conversion
|
|
511
|
+
// - has time component, no UTC marker → treat as user-local, subtract offset
|
|
512
|
+
// - date only → default to 09:00 user-local, subtract offset
|
|
513
|
+
//
|
|
514
|
+
// Critical: parses date/time COMPONENTS from the raw string via regex, NOT via
|
|
515
|
+
// `new Date(str)`. JS Date parsing of naive strings is TZ-dependent on the
|
|
516
|
+
// host clock — the VPS may be WIB or UTC, and we cannot trust either.
|
|
517
|
+
function normalizeStartDate(
|
|
518
|
+
startDate: string,
|
|
519
|
+
explicitScheduledTime: string | undefined,
|
|
520
|
+
tzOffsetHours: number,
|
|
521
|
+
): {
|
|
522
|
+
startDateUTC: string;
|
|
523
|
+
nextRunAtEpoch: number;
|
|
524
|
+
scheduledTimeLocal: string;
|
|
525
|
+
scheduledDateLocal: string;
|
|
526
|
+
} | null {
|
|
527
|
+
const trimmed = startDate.trim();
|
|
528
|
+
const dateMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
529
|
+
if (!dateMatch) return null;
|
|
530
|
+
const year = parseInt(dateMatch[1], 10);
|
|
531
|
+
const month = parseInt(dateMatch[2], 10);
|
|
532
|
+
const day = parseInt(dateMatch[3], 10);
|
|
533
|
+
|
|
534
|
+
const utcMarker = /Z$|[+-]00:?00$|UTC$/i.test(trimmed);
|
|
535
|
+
const timeMatch = trimmed.match(/[T ](\d{2}):(\d{2})(?::(\d{2}))?/);
|
|
536
|
+
const offsetMin = Math.round(tzOffsetHours * 60);
|
|
537
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
538
|
+
|
|
539
|
+
// explicit scheduled_time: agent passed local time alongside a date.
|
|
540
|
+
if (explicitScheduledTime) {
|
|
541
|
+
const stMatch = explicitScheduledTime.match(/^(\d{2}):(\d{2})/);
|
|
542
|
+
const localH = stMatch ? parseInt(stMatch[1], 10) : 0;
|
|
543
|
+
const localM = stMatch ? parseInt(stMatch[2], 10) : 0;
|
|
544
|
+
const utcMs = Date.UTC(year, month - 1, day, localH, localM, 0) - offsetMin * 60 * 1000;
|
|
545
|
+
return {
|
|
546
|
+
startDateUTC: new Date(utcMs).toISOString(),
|
|
547
|
+
nextRunAtEpoch: Math.floor(utcMs / 1000),
|
|
548
|
+
scheduledTimeLocal: `${pad(localH)}:${pad(localM)}`,
|
|
549
|
+
scheduledDateLocal: `${year}-${pad(month)}-${pad(day)}`,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (utcMarker && timeMatch) {
|
|
554
|
+
// Input is already UTC. Use components verbatim.
|
|
555
|
+
const utcH = parseInt(timeMatch[1], 10);
|
|
556
|
+
const utcM = parseInt(timeMatch[2], 10);
|
|
557
|
+
const utcS = timeMatch[3] ? parseInt(timeMatch[3], 10) : 0;
|
|
558
|
+
const utcMs = Date.UTC(year, month - 1, day, utcH, utcM, utcS);
|
|
559
|
+
// Recover local clock by adding the user's offset.
|
|
560
|
+
const localMs = utcMs + offsetMin * 60 * 1000;
|
|
561
|
+
const local = new Date(localMs);
|
|
562
|
+
return {
|
|
563
|
+
startDateUTC: new Date(utcMs).toISOString(),
|
|
564
|
+
nextRunAtEpoch: Math.floor(utcMs / 1000),
|
|
565
|
+
scheduledTimeLocal: `${pad(local.getUTCHours())}:${pad(local.getUTCMinutes())}`,
|
|
566
|
+
scheduledDateLocal: `${local.getUTCFullYear()}-${pad(local.getUTCMonth() + 1)}-${pad(local.getUTCDate())}`,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (timeMatch) {
|
|
571
|
+
// Local-no-Z input: user-local time, subtract offset to get UTC.
|
|
572
|
+
const localH = parseInt(timeMatch[1], 10);
|
|
573
|
+
const localM = parseInt(timeMatch[2], 10);
|
|
574
|
+
const localS = timeMatch[3] ? parseInt(timeMatch[3], 10) : 0;
|
|
575
|
+
const utcMs = Date.UTC(year, month - 1, day, localH, localM, localS) - offsetMin * 60 * 1000;
|
|
576
|
+
return {
|
|
577
|
+
startDateUTC: new Date(utcMs).toISOString(),
|
|
578
|
+
nextRunAtEpoch: Math.floor(utcMs / 1000),
|
|
579
|
+
scheduledTimeLocal: `${pad(localH)}:${pad(localM)}`,
|
|
580
|
+
scheduledDateLocal: `${year}-${pad(month)}-${pad(day)}`,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Date-only input — default to 09:00 user-local.
|
|
585
|
+
const utcMs = Date.UTC(year, month - 1, day, 9, 0, 0) - offsetMin * 60 * 1000;
|
|
586
|
+
return {
|
|
587
|
+
startDateUTC: new Date(utcMs).toISOString(),
|
|
588
|
+
nextRunAtEpoch: Math.floor(utcMs / 1000),
|
|
589
|
+
scheduledTimeLocal: "09:00",
|
|
590
|
+
scheduledDateLocal: `${year}-${pad(month)}-${pad(day)}`,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
461
594
|
async function handleCreateTask(
|
|
462
595
|
supabase: SupabaseClient,
|
|
463
596
|
userId: string,
|
|
464
597
|
resolveAgent: (id?: string) => Promise<string | null>,
|
|
465
598
|
params: Record<string, unknown>,
|
|
466
|
-
|
|
599
|
+
fallbackTimezone: string = "Asia/Jakarta",
|
|
467
600
|
): Promise<ToolResult> {
|
|
468
601
|
try {
|
|
469
602
|
if (!params.title) return err("Missing required field: title");
|
|
@@ -471,6 +604,11 @@ async function handleCreateTask(
|
|
|
471
604
|
const id = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
472
605
|
const now = new Date().toISOString();
|
|
473
606
|
|
|
607
|
+
// BUG 5 fix: resolve user's IANA timezone from their profile row. Plugin
|
|
608
|
+
// config default is only used when the profile row is missing/unset.
|
|
609
|
+
const timezone = await loadUserTimezone(supabase, userId, fallbackTimezone);
|
|
610
|
+
const TZ_OFFSET_HOURS = getTimezoneOffsetHours(timezone);
|
|
611
|
+
|
|
474
612
|
// Handle explicit "none"/"unassigned"
|
|
475
613
|
const rawAgentId = params.agent_id as string | undefined;
|
|
476
614
|
const isUnassigned =
|
|
@@ -576,6 +714,17 @@ async function handleCreateTask(
|
|
|
576
714
|
}
|
|
577
715
|
}
|
|
578
716
|
|
|
717
|
+
// BUG 5 fix (cycle 7b BUGSHOOT-2): normalize start_date BEFORE the INSERT
|
|
718
|
+
// so DB and tool-response carry the SAME canonical UTC ISO. Old code
|
|
719
|
+
// INSERTed the raw input then reassigned insertData.start_date afterward,
|
|
720
|
+
// which left the DB row with the unconverted value and the response with
|
|
721
|
+
// the converted one — confusing and unsafe for the scheduler.
|
|
722
|
+
const rawStartDate = (params.start_date as string) || null;
|
|
723
|
+
const normalized = rawStartDate
|
|
724
|
+
? normalizeStartDate(rawStartDate, params.scheduled_time as string | undefined, TZ_OFFSET_HOURS)
|
|
725
|
+
: null;
|
|
726
|
+
const canonicalStartDate = normalized?.startDateUTC ?? rawStartDate;
|
|
727
|
+
|
|
579
728
|
const insertData: Record<string, unknown> = {
|
|
580
729
|
id,
|
|
581
730
|
user_id: userId,
|
|
@@ -588,8 +737,8 @@ async function handleCreateTask(
|
|
|
588
737
|
priority: params.priority !== undefined ? params.priority : 1,
|
|
589
738
|
space_id: resolvedSpaceId,
|
|
590
739
|
folder_id: resolvedFolderId,
|
|
591
|
-
start_date:
|
|
592
|
-
due_date: (params.due_date as string) ||
|
|
740
|
+
start_date: canonicalStartDate,
|
|
741
|
+
due_date: (params.due_date as string) || canonicalStartDate || null,
|
|
593
742
|
tags: (params.tags as string[]) || [],
|
|
594
743
|
progress: 0,
|
|
595
744
|
sort_order: 0,
|
|
@@ -615,113 +764,15 @@ async function handleCreateTask(
|
|
|
615
764
|
}
|
|
616
765
|
|
|
617
766
|
// ── Auto-create scheduler event if task has a start_date ──────────────
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
|
|
621
|
-
// TIMEZONE: scheduled_time is treated as the user's LOCAL time (default WIB, UTC+7).
|
|
622
|
-
// We convert to UTC epoch for next_run_at so the edge function fires correctly.
|
|
623
|
-
const startDate = params.start_date as string | undefined;
|
|
767
|
+
// start_date is already normalized above. Scheduler event uses the same
|
|
768
|
+
// canonical UTC ISO + the local-clock components for the dashboard UI.
|
|
769
|
+
const startDate = rawStartDate;
|
|
624
770
|
const effectiveAgentId = (insertData.agent_id as string) || assignee;
|
|
625
|
-
|
|
626
|
-
// Resolve timezone offset from IANA timezone string (configurable per user)
|
|
627
|
-
const getTimezoneOffsetHours = (tz: string): number => {
|
|
628
|
-
try {
|
|
629
|
-
const now = new Date();
|
|
630
|
-
const utcStr = now.toLocaleString('en-US', { timeZone: 'UTC' });
|
|
631
|
-
const tzStr = now.toLocaleString('en-US', { timeZone: tz });
|
|
632
|
-
const diffMs = new Date(tzStr).getTime() - new Date(utcStr).getTime();
|
|
633
|
-
return Math.round(diffMs / 3600000);
|
|
634
|
-
} catch {
|
|
635
|
-
return 7; // Fallback to WIB (UTC+7)
|
|
636
|
-
}
|
|
637
|
-
};
|
|
638
|
-
const TZ_OFFSET_HOURS = getTimezoneOffsetHours(timezone);
|
|
639
|
-
if (startDate && effectiveAgentId) {
|
|
771
|
+
if (startDate && effectiveAgentId && normalized) {
|
|
640
772
|
try {
|
|
641
|
-
|
|
642
|
-
// "2026-04-19" (date only)
|
|
643
|
-
// "2026-04-19T18:45:00" (local datetime)
|
|
644
|
-
// "2026-04-19 11:45:00+00" (Supabase timestamptz)
|
|
645
|
-
// "2026-04-19T11:45:00.000Z" (ISO UTC)
|
|
646
|
-
const parsedDate = new Date(startDate);
|
|
647
|
-
const explicitScheduledTime = params.scheduled_time as string | undefined;
|
|
648
|
-
|
|
649
|
-
let nextRunAtEpoch: number;
|
|
650
|
-
let scheduledTimeFinal: string;
|
|
651
|
-
let scheduledDateFinal: string;
|
|
652
|
-
|
|
653
|
-
if (!isNaN(parsedDate.getTime())) {
|
|
654
|
-
// Valid date — check if it includes a meaningful time component
|
|
655
|
-
const hasTimeInfo = /[T ]\d{2}:\d{2}/.test(startDate);
|
|
656
|
-
// BUG 4 fix (cycle 7b BUGSHOOT-1): if the input string already carries
|
|
657
|
-
// an explicit UTC marker (Z, +00, +0000, +00:00, UTC), the time
|
|
658
|
-
// component is NOT user-local — skip the local→UTC subtraction.
|
|
659
|
-
// Without this, a UTC-Z input like "2026-05-10T16:25:00Z" got mistaken
|
|
660
|
-
// for local 16:25 and stored as 09:25Z (off by TZ_OFFSET_HOURS).
|
|
661
|
-
const isUtcInput = /Z$|[+-]00:?00$|UTC$/i.test(startDate.trim());
|
|
662
|
-
|
|
663
|
-
if (explicitScheduledTime) {
|
|
664
|
-
// Agent explicitly passed a scheduled_time — treat as user’s local time
|
|
665
|
-
const dateStr = parsedDate.toISOString().split("T")[0]; // YYYY-MM-DD
|
|
666
|
-
const [localH, localM] = explicitScheduledTime.split(":").map(Number);
|
|
667
|
-
const utcH = localH - TZ_OFFSET_HOURS;
|
|
668
|
-
const dt = new Date(`${dateStr}T00:00:00Z`);
|
|
669
|
-
dt.setUTCHours(utcH, localM, 0, 0);
|
|
670
|
-
nextRunAtEpoch = Math.floor(dt.getTime() / 1000);
|
|
671
|
-
scheduledTimeFinal = explicitScheduledTime; // Store as user's local time
|
|
672
|
-
scheduledDateFinal = dateStr;
|
|
673
|
-
// Normalize start_date to ISO UTC for consistent downstream parsing
|
|
674
|
-
insertData.start_date = dt.toISOString();
|
|
675
|
-
} else if (isUtcInput) {
|
|
676
|
-
// Input is already UTC — no local→UTC conversion. Use parsedDate
|
|
677
|
-
// directly. scheduled_time is documented as user-local, so add the
|
|
678
|
-
// offset to recover the local-clock value.
|
|
679
|
-
nextRunAtEpoch = Math.floor(parsedDate.getTime() / 1000);
|
|
680
|
-
const localHour = (parsedDate.getUTCHours() + TZ_OFFSET_HOURS + 24) % 24;
|
|
681
|
-
scheduledTimeFinal = `${String(localHour).padStart(2, "0")}:${String(parsedDate.getUTCMinutes()).padStart(2, "0")}`;
|
|
682
|
-
scheduledDateFinal = parsedDate.toISOString().split("T")[0];
|
|
683
|
-
insertData.start_date = parsedDate.toISOString();
|
|
684
|
-
} else if (hasTimeInfo) {
|
|
685
|
-
// start_date already contains time — assume it’s in the user’s local timezone
|
|
686
|
-
// Extract the local hour:minute from the string, NOT from UTC parsing
|
|
687
|
-
const timeMatch = startDate.match(/(\d{2}):(\d{2})/);
|
|
688
|
-
if (timeMatch) {
|
|
689
|
-
const localH = parseInt(timeMatch[1], 10);
|
|
690
|
-
const localM = parseInt(timeMatch[2], 10);
|
|
691
|
-
const dateStr = parsedDate.toISOString().split("T")[0];
|
|
692
|
-
const dt = new Date(`${dateStr}T00:00:00Z`);
|
|
693
|
-
dt.setUTCHours(localH - TZ_OFFSET_HOURS, localM, 0, 0);
|
|
694
|
-
nextRunAtEpoch = Math.floor(dt.getTime() / 1000);
|
|
695
|
-
scheduledTimeFinal = `${String(localH).padStart(2, "0")}:${String(localM).padStart(2, "0")}`;
|
|
696
|
-
scheduledDateFinal = dateStr;
|
|
697
|
-
// Normalize start_date to ISO UTC
|
|
698
|
-
insertData.start_date = dt.toISOString();
|
|
699
|
-
} else {
|
|
700
|
-
// Can't extract time — fall back to UTC parsing
|
|
701
|
-
nextRunAtEpoch = Math.floor(parsedDate.getTime() / 1000);
|
|
702
|
-
scheduledTimeFinal = `${String(parsedDate.getUTCHours()).padStart(2, "0")}:${String(parsedDate.getUTCMinutes()).padStart(2, "0")}`;
|
|
703
|
-
scheduledDateFinal = parsedDate.toISOString().split("T")[0];
|
|
704
|
-
}
|
|
705
|
-
} else {
|
|
706
|
-
// Date only, no time — default to 09:00 user local time
|
|
707
|
-
const dateStr = parsedDate.toISOString().split("T")[0];
|
|
708
|
-
const dt = new Date(`${dateStr}T00:00:00Z`);
|
|
709
|
-
dt.setUTCHours(9 - TZ_OFFSET_HOURS, 0, 0, 0); // 09:00 local = UTC - offset
|
|
710
|
-
nextRunAtEpoch = Math.floor(dt.getTime() / 1000);
|
|
711
|
-
scheduledTimeFinal = "09:00"; // Stored as user's local time
|
|
712
|
-
scheduledDateFinal = dateStr;
|
|
713
|
-
// Normalize start_date to ISO UTC
|
|
714
|
-
insertData.start_date = dt.toISOString();
|
|
715
|
-
}
|
|
716
|
-
} else {
|
|
717
|
-
// Unparseable date — fallback to now + 60s
|
|
718
|
-
nextRunAtEpoch = Math.floor(Date.now() / 1000) + 60;
|
|
719
|
-
scheduledTimeFinal = "00:00";
|
|
720
|
-
scheduledDateFinal = new Date().toISOString().split("T")[0];
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// Safety net: if computed time is in the past, schedule for now + 60s
|
|
773
|
+
let { nextRunAtEpoch } = normalized;
|
|
724
774
|
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
775
|
+
// Safety net: if computed time is in the past, schedule for now + 60s
|
|
725
776
|
if (nextRunAtEpoch <= nowEpoch) {
|
|
726
777
|
nextRunAtEpoch = nowEpoch + 60;
|
|
727
778
|
}
|
|
@@ -734,8 +785,8 @@ async function handleCreateTask(
|
|
|
734
785
|
subagent_id: subagentId,
|
|
735
786
|
title: params.title,
|
|
736
787
|
description: (params.description as string) || (params.instructions as string) || null,
|
|
737
|
-
scheduled_date:
|
|
738
|
-
scheduled_time:
|
|
788
|
+
scheduled_date: normalized.scheduledDateLocal,
|
|
789
|
+
scheduled_time: normalized.scheduledTimeLocal,
|
|
739
790
|
duration_minutes: 30,
|
|
740
791
|
recurrence_type: (params.recurrence_type as string) || "none",
|
|
741
792
|
recurrence_interval: (params.recurrence_interval as number) || 1,
|
|
@@ -6573,7 +6624,24 @@ function registerBrainExtractionHook(
|
|
|
6573
6624
|
// scoped memory note ("Staff X reported on task Y: ...") and POST a
|
|
6574
6625
|
// webhook so the dashboard can surface the report. The staff itself
|
|
6575
6626
|
// gets no memory entry (req 3 — staff has no memory of its own).
|
|
6576
|
-
|
|
6627
|
+
//
|
|
6628
|
+
// Cycle 7b BUGSHOOT-2 (BUG 7) — metadata path is dead since BUG 2 made
|
|
6629
|
+
// the dispatcher stop sending `metadata` on chat.send. Fall back to
|
|
6630
|
+
// the dispatch_context row keyed by sessionKey before giving up.
|
|
6631
|
+
let staffDispatchSubagentId = readDispatchSubagentId(ctx, event);
|
|
6632
|
+
let dispatchCtxRow: DispatchContextRow | null = null;
|
|
6633
|
+
let dispatchTaskIdFromCtx: string | null = null;
|
|
6634
|
+
let dispatchConversationIdFromCtx: string | null = null;
|
|
6635
|
+
if (sessionKey) {
|
|
6636
|
+
dispatchCtxRow = await loadDispatchContextBySession(supabase, userId, sessionKey).catch(() => null);
|
|
6637
|
+
if (dispatchCtxRow) {
|
|
6638
|
+
if (!staffDispatchSubagentId && dispatchCtxRow.subagent_id) {
|
|
6639
|
+
staffDispatchSubagentId = dispatchCtxRow.subagent_id;
|
|
6640
|
+
}
|
|
6641
|
+
dispatchTaskIdFromCtx = dispatchCtxRow.task_id;
|
|
6642
|
+
dispatchConversationIdFromCtx = dispatchCtxRow.conversation_id;
|
|
6643
|
+
}
|
|
6644
|
+
}
|
|
6577
6645
|
if (staffDispatchSubagentId) {
|
|
6578
6646
|
(async () => {
|
|
6579
6647
|
try {
|
|
@@ -6586,9 +6654,12 @@ function registerBrainExtractionHook(
|
|
|
6586
6654
|
api.logger.warn?.(`[ofiere-staff-report] chief_mismatch (subagent ${staffDispatchSubagentId} reports to ${staff.chief_agent_id}, run was on ${resolvedAgentId}) — skipping report`);
|
|
6587
6655
|
return;
|
|
6588
6656
|
}
|
|
6657
|
+
// BUG 7 fix (BUGSHOOT-2): include dispatch_context.task_id as a
|
|
6658
|
+
// fallback now that metadata is dead on the wire.
|
|
6589
6659
|
const taskId =
|
|
6590
6660
|
ctx?.metadata?.task_id || ctx?.metadata?.dispatch?.task_id ||
|
|
6591
|
-
event?.metadata?.task_id || event?.context?.metadata?.task_id ||
|
|
6661
|
+
event?.metadata?.task_id || event?.context?.metadata?.task_id ||
|
|
6662
|
+
dispatchTaskIdFromCtx || null;
|
|
6592
6663
|
const excerpt = lastAssistant.slice(0, 1500);
|
|
6593
6664
|
const reportBody = taskId
|
|
6594
6665
|
? `Staff ${staff.name}${staff.role ? ` (${staff.role})` : ""} reported on task ${taskId}:\n\n${excerpt}`
|
|
@@ -6604,6 +6675,54 @@ function registerBrainExtractionHook(
|
|
|
6604
6675
|
});
|
|
6605
6676
|
api.logger.info?.(`[ofiere-staff-report] memory written for chief ${staff.chief_agent_id} from staff ${staff.name}`);
|
|
6606
6677
|
|
|
6678
|
+
// BUG 9 fix (BUGSHOOT-2): mark task_dispatch_log row complete
|
|
6679
|
+
// with the assistant's response preview. Without this the log
|
|
6680
|
+
// stays at status='dispatched' forever, breaking dashboard
|
|
6681
|
+
// dispatch-history views and audit trails.
|
|
6682
|
+
if (taskId) {
|
|
6683
|
+
try {
|
|
6684
|
+
await supabase.from("task_dispatch_log")
|
|
6685
|
+
.update({
|
|
6686
|
+
status: "completed",
|
|
6687
|
+
response_preview: excerpt.slice(0, 500),
|
|
6688
|
+
completed_at: new Date().toISOString(),
|
|
6689
|
+
})
|
|
6690
|
+
.eq("task_id", taskId)
|
|
6691
|
+
.eq("status", "dispatched");
|
|
6692
|
+
} catch (logErr) {
|
|
6693
|
+
api.logger.debug?.(`[ofiere-staff-report] dispatch_log update failed: ${logErr instanceof Error ? logErr.message : String(logErr)}`);
|
|
6694
|
+
}
|
|
6695
|
+
}
|
|
6696
|
+
|
|
6697
|
+
// BUG 6 fix (BUGSHOOT-2): mirror the assistant turn into the
|
|
6698
|
+
// dashboard's hidden conversation_messages so the dashboard can
|
|
6699
|
+
// render staff-run history. Dispatcher pre-created the
|
|
6700
|
+
// conversations row and stamped its id on dispatch_context.
|
|
6701
|
+
if (dispatchConversationIdFromCtx) {
|
|
6702
|
+
try {
|
|
6703
|
+
await supabase.from("conversation_messages").insert({
|
|
6704
|
+
conversation_id: dispatchConversationIdFromCtx,
|
|
6705
|
+
user_id: userId,
|
|
6706
|
+
role: "user",
|
|
6707
|
+
content: lastUser.slice(0, 4000),
|
|
6708
|
+
created_at: new Date(Date.now() - 1000).toISOString(),
|
|
6709
|
+
});
|
|
6710
|
+
await supabase.from("conversation_messages").insert({
|
|
6711
|
+
conversation_id: dispatchConversationIdFromCtx,
|
|
6712
|
+
user_id: userId,
|
|
6713
|
+
role: "assistant",
|
|
6714
|
+
content: excerpt,
|
|
6715
|
+
created_at: new Date().toISOString(),
|
|
6716
|
+
});
|
|
6717
|
+
await supabase.from("conversations")
|
|
6718
|
+
.update({ updated_at: new Date().toISOString() })
|
|
6719
|
+
.eq("id", dispatchConversationIdFromCtx)
|
|
6720
|
+
.eq("user_id", userId);
|
|
6721
|
+
} catch (cmErr) {
|
|
6722
|
+
api.logger.debug?.(`[ofiere-staff-report] conversation_messages insert failed: ${cmErr instanceof Error ? cmErr.message : String(cmErr)}`);
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
|
|
6607
6726
|
// Best-effort webhook to dashboard (existing OPENCLAW_WEBHOOK_SECRET).
|
|
6608
6727
|
const webhookUrl = process.env.OFIERE_DASHBOARD_WEBHOOK_URL || "";
|
|
6609
6728
|
const webhookSecret = process.env.OPENCLAW_WEBHOOK_SECRET || "";
|
|
@@ -6639,6 +6758,24 @@ function registerBrainExtractionHook(
|
|
|
6639
6758
|
return;
|
|
6640
6759
|
}
|
|
6641
6760
|
|
|
6761
|
+
// BUG 9 fix (BUGSHOOT-2): mark task_dispatch_log row complete for
|
|
6762
|
+
// non-staff scheduled dispatches too. Without this the log row stays
|
|
6763
|
+
// at 'dispatched' even after the chief finished the run.
|
|
6764
|
+
if (dispatchTaskIdFromCtx) {
|
|
6765
|
+
try {
|
|
6766
|
+
await supabase.from("task_dispatch_log")
|
|
6767
|
+
.update({
|
|
6768
|
+
status: "completed",
|
|
6769
|
+
response_preview: lastAssistant.slice(0, 500),
|
|
6770
|
+
completed_at: new Date().toISOString(),
|
|
6771
|
+
})
|
|
6772
|
+
.eq("task_id", dispatchTaskIdFromCtx)
|
|
6773
|
+
.eq("status", "dispatched");
|
|
6774
|
+
} catch (logErr) {
|
|
6775
|
+
api.logger.debug?.(`[ofiere-brain] dispatch_log update failed: ${logErr instanceof Error ? logErr.message : String(logErr)}`);
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
6778
|
+
|
|
6642
6779
|
// ── Fast Stream: Write L1_focus + L2_episode in parallel ──
|
|
6643
6780
|
const rawContent = `User: ${lastUser}\nAssistant: ${lastAssistant}`;
|
|
6644
6781
|
const immediateWrites: PromiseLike<any>[] = [];
|